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:
|
develop:
|
||||||
name: Build Nightly Docker if Develop push
|
name: Build Nightly Docker if Develop push
|
||||||
needs: [ build, test, version ]
|
needs: [ build, version ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||||
steps:
|
steps:
|
||||||
@ -232,7 +232,7 @@ jobs:
|
|||||||
|
|
||||||
stable:
|
stable:
|
||||||
name: Build Stable Docker if Main push
|
name: Build Stable Docker if Main push
|
||||||
needs: [ build, test ]
|
needs: [ build ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||||
steps:
|
steps:
|
||||||
|
@ -38,6 +38,7 @@ public class CleanupServiceTests
|
|||||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||||
private const string BackupDirectory = "C:/kavita/config/backups/";
|
private const string BackupDirectory = "C:/kavita/config/backups/";
|
||||||
|
private const string LogDirectory = "C:/kavita/config/logs/";
|
||||||
private const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
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 = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync();
|
||||||
setting.Value = BookmarkDirectory;
|
setting.Value = BookmarkDirectory;
|
||||||
|
|
||||||
|
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.TotalLogs).SingleAsync();
|
||||||
|
setting.Value = "10";
|
||||||
|
|
||||||
_context.ServerSetting.Update(setting);
|
_context.ServerSetting.Update(setting);
|
||||||
|
|
||||||
_context.Library.Add(new Library()
|
_context.Library.Add(new Library()
|
||||||
@ -412,6 +416,59 @@ public class CleanupServiceTests
|
|||||||
|
|
||||||
#endregion
|
#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
|
// #region CleanupBookmarks
|
||||||
//
|
//
|
||||||
// [Fact]
|
// [Fact]
|
||||||
|
@ -895,6 +895,60 @@ public class ReaderServiceTests
|
|||||||
Assert.Equal("1", actualChapter.Range);
|
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]
|
[Fact]
|
||||||
public async Task GetPrevChapterIdAsync_ShouldRollIntoPrevVolume()
|
public async Task GetPrevChapterIdAsync_ShouldRollIntoPrevVolume()
|
||||||
{
|
{
|
||||||
|
@ -85,19 +85,19 @@ public class SeriesServiceTests
|
|||||||
|
|
||||||
_context.ServerSetting.Update(setting);
|
_context.ServerSetting.Update(setting);
|
||||||
|
|
||||||
var lib = new Library()
|
// var lib = new Library()
|
||||||
{
|
// {
|
||||||
Name = "Manga", Folders = new List<FolderPath>() {new FolderPath() {Path = "C:/data/"}}
|
// Name = "Manga", Folders = new List<FolderPath>() {new FolderPath() {Path = "C:/data/"}}
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
_context.AppUser.Add(new AppUser()
|
// _context.AppUser.Add(new AppUser()
|
||||||
{
|
// {
|
||||||
UserName = "majora2007",
|
// UserName = "majora2007",
|
||||||
Libraries = new List<Library>()
|
// Libraries = new List<Library>()
|
||||||
{
|
// {
|
||||||
lib
|
// lib
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
return await _context.SaveChangesAsync() > 0;
|
return await _context.SaveChangesAsync() > 0;
|
||||||
}
|
}
|
||||||
@ -109,6 +109,7 @@ public class SeriesServiceTests
|
|||||||
_context.Genre.RemoveRange(_context.Genre.ToList());
|
_context.Genre.RemoveRange(_context.Genre.ToList());
|
||||||
_context.CollectionTag.RemoveRange(_context.CollectionTag.ToList());
|
_context.CollectionTag.RemoveRange(_context.CollectionTag.ToList());
|
||||||
_context.Person.RemoveRange(_context.Person.ToList());
|
_context.Person.RemoveRange(_context.Person.ToList());
|
||||||
|
_context.Library.RemoveRange(_context.Library.ToList());
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
@ -135,33 +136,45 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Manga,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
new AppUser()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("Omake", true, new List<MangaFile>()),
|
UserName = "majora2007"
|
||||||
EntityFactory.CreateChapter("Something SP02", true, new List<MangaFile>()),
|
}
|
||||||
}),
|
},
|
||||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
Name = "Test",
|
||||||
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
Volumes = new List<Volume>()
|
||||||
}),
|
{
|
||||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
EntityFactory.CreateChapter("Omake", true, new List<MangaFile>()),
|
||||||
EntityFactory.CreateChapter("32", false, 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();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
var expectedRanges = new[] {"Omake", "Something SP02"};
|
var expectedRanges = new[] {"Omake", "Something SP02"};
|
||||||
@ -177,30 +190,41 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Manga,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
new AppUser()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
UserName = "majora2007"
|
||||||
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
}
|
||||||
}),
|
},
|
||||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
Name = "Test",
|
||||||
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
Volumes = new List<Volume>()
|
||||||
}),
|
{
|
||||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
EntityFactory.CreateChapter("32", 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();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Manga,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
new AppUser()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
UserName = "majora2007"
|
||||||
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
}
|
||||||
}),
|
},
|
||||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
Name = "Test",
|
||||||
}),
|
Volumes = new List<Volume>()
|
||||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
{
|
||||||
{
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
{
|
||||||
}),
|
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();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Manga,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
new AppUser()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
UserName = "majora2007"
|
||||||
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
}
|
||||||
}),
|
},
|
||||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
Name = "Test",
|
||||||
}),
|
Volumes = new List<Volume>()
|
||||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
{
|
||||||
{
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
{
|
||||||
}),
|
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();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Book,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
new AppUser()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
UserName = "majora2007"
|
||||||
}),
|
}
|
||||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
},
|
||||||
|
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();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
||||||
@ -339,26 +397,39 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Book,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
new AppUser()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", true, new List<MangaFile>()),
|
UserName = "majora2007"
|
||||||
}),
|
}
|
||||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
},
|
||||||
|
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();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
||||||
@ -379,36 +450,48 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Book,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
new AppUser()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
UserName = "majora2007"
|
||||||
}),
|
}
|
||||||
EntityFactory.CreateVolume("1.2", new List<Chapter>()
|
},
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
Name = "Test",
|
||||||
}),
|
Volumes = new List<Volume>()
|
||||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
{
|
||||||
{
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
{
|
||||||
}),
|
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();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
||||||
Assert.Equal("1", detail.Volumes.ElementAt(0).Name);
|
Assert.Equal("Volume 1", detail.Volumes.ElementAt(0).Name);
|
||||||
Assert.Equal("1.2", detail.Volumes.ElementAt(1).Name);
|
Assert.Equal("Volume 1.2", detail.Volumes.ElementAt(1).Name);
|
||||||
Assert.Equal("2", detail.Volumes.ElementAt(2).Name);
|
Assert.Equal("Volume 2", detail.Volumes.ElementAt(2).Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -422,28 +505,34 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Manga,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
|
||||||
{
|
{
|
||||||
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();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
@ -470,23 +559,28 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Manga,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
|
||||||
{
|
{
|
||||||
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();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Manga,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
|
||||||
{
|
{
|
||||||
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();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Manga,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
|
||||||
{
|
{
|
||||||
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
|
#region UpdateSeriesMetadata
|
||||||
|
|
||||||
private void SetupUpdateSeriesMetadataDb()
|
|
||||||
{
|
|
||||||
_context.Series.Add(new Series()
|
|
||||||
{
|
|
||||||
Name = "Test",
|
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Book,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UpdateSeriesMetadata_ShouldCreateEmptyMetadata_IfDoesntExist()
|
public async Task UpdateSeriesMetadata_ShouldCreateEmptyMetadata_IfDoesntExist()
|
||||||
{
|
{
|
||||||
|
@ -9,7 +9,7 @@ namespace API.Controllers;
|
|||||||
public class HealthController : BaseApiController
|
public class HealthController : BaseApiController
|
||||||
{
|
{
|
||||||
|
|
||||||
[HttpGet()]
|
[HttpGet]
|
||||||
public ActionResult GetHealth()
|
public ActionResult GetHealth()
|
||||||
{
|
{
|
||||||
return Ok("Ok");
|
return Ok("Ok");
|
||||||
|
@ -223,6 +223,16 @@ public class SettingsController : BaseApiController
|
|||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_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)
|
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
|
||||||
{
|
{
|
||||||
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
|
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;
|
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.
|
/// If the Swagger UI Should be exposed. Does not require authentication, but does require a JWT.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableSwaggerUi { get; set; }
|
public bool EnableSwaggerUi { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of Backups before cleanup
|
/// The amount of Backups before cleanup
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -60,4 +60,9 @@ public class ServerSettingDto
|
|||||||
/// If Kavita should watch the library folders and process changes
|
/// If Kavita should watch the library folders and process changes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableFolderWatching { get; set; } = true;
|
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;
|
namespace API.DTOs.Stats;
|
||||||
|
|
||||||
@ -118,4 +120,24 @@ public class ServerInfoDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Introduced in v0.5.4</remarks>
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
public bool UsingSeriesRelationships { get; set; }
|
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<IEnumerable<Library>> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None);
|
||||||
Task<bool> DeleteLibrary(int libraryId);
|
Task<bool> DeleteLibrary(int libraryId);
|
||||||
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
|
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
|
||||||
|
Task<IEnumerable<int>> GetLibraryIdsForUserIdAsync(int userId);
|
||||||
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
|
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
|
||||||
Task<IEnumerable<Library>> GetLibraryForIdsAsync(IEnumerable<int> libraryIds, LibraryIncludes includes = LibraryIncludes.None);
|
Task<IEnumerable<Library>> GetLibraryForIdsAsync(IEnumerable<int> libraryIds, LibraryIncludes includes = LibraryIncludes.None);
|
||||||
Task<int> GetTotalFiles();
|
Task<int> GetTotalFiles();
|
||||||
@ -111,6 +112,11 @@ public class LibraryRepository : ILibraryRepository
|
|||||||
return await _context.SaveChangesAsync() > 0;
|
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)
|
public async Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId)
|
||||||
{
|
{
|
||||||
return await _context.Library
|
return await _context.Library
|
||||||
@ -120,6 +126,14 @@ public class LibraryRepository : ILibraryRepository
|
|||||||
.ToListAsync();
|
.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)
|
public async Task<LibraryType> GetLibraryTypeAsync(int libraryId)
|
||||||
{
|
{
|
||||||
return await _context.Library
|
return await _context.Library
|
||||||
|
@ -103,6 +103,7 @@ public static class Seed
|
|||||||
new() {Key = ServerSettingKey.ConvertBookmarkToWebP, Value = "false"},
|
new() {Key = ServerSettingKey.ConvertBookmarkToWebP, Value = "false"},
|
||||||
new() {Key = ServerSettingKey.EnableSwaggerUi, Value = "false"},
|
new() {Key = ServerSettingKey.EnableSwaggerUi, Value = "false"},
|
||||||
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
|
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
|
||||||
|
new() {Key = ServerSettingKey.TotalLogs, Value = "30"},
|
||||||
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
||||||
}.ToArray());
|
}.ToArray());
|
||||||
|
|
||||||
|
@ -96,4 +96,9 @@ public enum ServerSettingKey
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Description("EnableFolderWatching")]
|
[Description("EnableFolderWatching")]
|
||||||
EnableFolderWatching = 17,
|
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:
|
case ServerSettingKey.EnableFolderWatching:
|
||||||
destination.EnableFolderWatching = bool.Parse(row.Value);
|
destination.EnableFolderWatching = bool.Parse(row.Value);
|
||||||
break;
|
break;
|
||||||
|
case ServerSettingKey.TotalLogs:
|
||||||
|
destination.TotalLogs = int.Parse(row.Value);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ public static class LogLevelOptions
|
|||||||
.WriteTo.File(LogFile,
|
.WriteTo.File(LogFile,
|
||||||
shared: true,
|
shared: true,
|
||||||
rollingInterval: RollingInterval.Day,
|
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)
|
public static void SwitchLogLevel(string level)
|
||||||
@ -60,26 +60,31 @@ public static class LogLevelOptions
|
|||||||
{
|
{
|
||||||
case "Debug":
|
case "Debug":
|
||||||
LogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
LogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
||||||
|
MicrosoftLogLevelSwitch.MinimumLevel = LogEventLevel.Information;
|
||||||
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
||||||
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
||||||
break;
|
break;
|
||||||
case "Information":
|
case "Information":
|
||||||
LogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
LogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||||
|
MicrosoftLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||||
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||||
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||||
break;
|
break;
|
||||||
case "Trace":
|
case "Trace":
|
||||||
LogLevelSwitch.MinimumLevel = LogEventLevel.Verbose;
|
LogLevelSwitch.MinimumLevel = LogEventLevel.Verbose;
|
||||||
|
MicrosoftLogLevelSwitch.MinimumLevel = LogEventLevel.Information;
|
||||||
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
||||||
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
||||||
break;
|
break;
|
||||||
case "Warning":
|
case "Warning":
|
||||||
LogLevelSwitch.MinimumLevel = LogEventLevel.Warning;
|
LogLevelSwitch.MinimumLevel = LogEventLevel.Warning;
|
||||||
|
MicrosoftLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||||
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||||
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||||
break;
|
break;
|
||||||
case "Critical":
|
case "Critical":
|
||||||
LogLevelSwitch.MinimumLevel = LogEventLevel.Fatal;
|
LogLevelSwitch.MinimumLevel = LogEventLevel.Fatal;
|
||||||
|
MicrosoftLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||||
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||||
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||||
break;
|
break;
|
||||||
|
@ -332,7 +332,7 @@ public class ArchiveService : IArchiveService
|
|||||||
{
|
{
|
||||||
var filenameWithoutExtension = Path.GetFileNameWithoutExtension(name).ToLower();
|
var filenameWithoutExtension = Path.GetFileNameWithoutExtension(name).ToLower();
|
||||||
return !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(fullName)
|
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);
|
&& !filenameWithoutExtension.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,6 +313,7 @@ public class ReaderService : IReaderService
|
|||||||
if (chapterId > 0) return chapterId;
|
if (chapterId > 0) return chapterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var next = false;
|
||||||
foreach (var volume in volumes)
|
foreach (var volume in volumes)
|
||||||
{
|
{
|
||||||
if (volume.Number == currentVolume.Number && volume.Chapters.Count > 1)
|
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),
|
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer),
|
||||||
currentChapter.Range, dto => dto.Range);
|
currentChapter.Range, dto => dto.Range);
|
||||||
if (chapterId > 0) return chapterId;
|
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
|
// Handle Chapters within next Volume
|
||||||
// ! When selecting the chapter for the next volume, we need to make sure a c0 comes before a c1+
|
// ! 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;
|
if (chapterId > 0) return chapterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var next = false;
|
||||||
foreach (var volume in volumes)
|
foreach (var volume in volumes)
|
||||||
{
|
{
|
||||||
if (volume.Number == currentVolume.Number)
|
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(),
|
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting).Reverse(),
|
||||||
currentChapter.Range, dto => dto.Range);
|
currentChapter.Range, dto => dto.Range);
|
||||||
if (chapterId > 0) return chapterId;
|
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
|
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);
|
var lastChapter = volume.Chapters.MaxBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
||||||
|
@ -13,6 +13,7 @@ using API.Entities;
|
|||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Services;
|
namespace API.Services;
|
||||||
@ -462,6 +463,9 @@ public class SeriesService : ISeriesService
|
|||||||
public async Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId)
|
public async Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId)
|
||||||
{
|
{
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, 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 libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
||||||
|
@ -53,6 +53,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
public const string CleanupTaskId = "cleanup";
|
public const string CleanupTaskId = "cleanup";
|
||||||
public const string BackupTaskId = "backup";
|
public const string BackupTaskId = "backup";
|
||||||
public const string ScanLibrariesTaskId = "scan-libraries";
|
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");
|
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");
|
_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)
|
public void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false)
|
||||||
@ -131,11 +132,14 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanLibrary(libraryId, forceUpdate));
|
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()
|
public void CancelStatsTasks()
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Cancelling/Removing StatsTasks");
|
_logger.LogDebug("Stopping Stat collection as user has opted out");
|
||||||
|
RecurringJob.RemoveIfExists(ReportStatsTaskId);
|
||||||
RecurringJob.RemoveIfExists("report-stats");
|
_statsService.SendCancellation();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -25,6 +25,7 @@ public interface ICleanupService
|
|||||||
Task DeleteChapterCoverImages();
|
Task DeleteChapterCoverImages();
|
||||||
Task DeleteTagCoverImages();
|
Task DeleteTagCoverImages();
|
||||||
Task CleanupBackups();
|
Task CleanupBackups();
|
||||||
|
Task CleanupLogs();
|
||||||
void CleanupTemp();
|
void CleanupTemp();
|
||||||
/// <summary>
|
/// <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.
|
/// 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 SendProgress(0.7F, "Cleaning deleted cover images");
|
||||||
await DeleteTagCoverImages();
|
await DeleteTagCoverImages();
|
||||||
await DeleteReadingListCoverImages();
|
await DeleteReadingListCoverImages();
|
||||||
|
await SendProgress(0.8F, "Cleaning old logs");
|
||||||
|
await CleanupLogs();
|
||||||
await SendProgress(1F, "Cleanup finished");
|
await SendProgress(1F, "Cleanup finished");
|
||||||
_logger.LogInformation("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);
|
_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()
|
public void CleanupTemp()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Performing cleanup of Temp directory");
|
_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
|
/// This is just here to prevent GC from Disposing our watchers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IList<FileSystemWatcher> _fileWatchers = new List<FileSystemWatcher>();
|
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;
|
private readonly TimeSpan _queueWaitTime;
|
||||||
|
|
||||||
|
|
||||||
@ -109,7 +112,7 @@ public class LibraryWatcher : ILibraryWatcher
|
|||||||
|
|
||||||
public async Task StartWatching()
|
public async Task StartWatching()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Starting file watchers");
|
_logger.LogInformation("[LibraryWatcher] Starting file watchers");
|
||||||
|
|
||||||
_libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
|
_libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
|
||||||
.SelectMany(l => l.Folders)
|
.SelectMany(l => l.Folders)
|
||||||
@ -119,7 +122,7 @@ public class LibraryWatcher : ILibraryWatcher
|
|||||||
.ToList();
|
.ToList();
|
||||||
foreach (var libraryFolder in _libraryFolders)
|
foreach (var libraryFolder in _libraryFolders)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Watching {FolderPath}", libraryFolder);
|
_logger.LogDebug("[LibraryWatcher] Watching {FolderPath}", libraryFolder);
|
||||||
var watcher = new FileSystemWatcher(libraryFolder);
|
var watcher = new FileSystemWatcher(libraryFolder);
|
||||||
|
|
||||||
watcher.Changed += OnChanged;
|
watcher.Changed += OnChanged;
|
||||||
@ -138,17 +141,19 @@ public class LibraryWatcher : ILibraryWatcher
|
|||||||
|
|
||||||
_watcherDictionary[libraryFolder].Add(watcher);
|
_watcherDictionary[libraryFolder].Add(watcher);
|
||||||
}
|
}
|
||||||
|
_logger.LogInformation("[LibraryWatcher] Watching {Count} folders", _fileWatchers.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StopWatching()
|
public void StopWatching()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Stopping watching folders");
|
_logger.LogInformation("[LibraryWatcher] Stopping watching folders");
|
||||||
foreach (var fileSystemWatcher in _watcherDictionary.Values.SelectMany(watcher => watcher))
|
foreach (var fileSystemWatcher in _watcherDictionary.Values.SelectMany(watcher => watcher))
|
||||||
{
|
{
|
||||||
fileSystemWatcher.EnableRaisingEvents = false;
|
fileSystemWatcher.EnableRaisingEvents = false;
|
||||||
fileSystemWatcher.Changed -= OnChanged;
|
fileSystemWatcher.Changed -= OnChanged;
|
||||||
fileSystemWatcher.Created -= OnCreated;
|
fileSystemWatcher.Created -= OnCreated;
|
||||||
fileSystemWatcher.Deleted -= OnDeleted;
|
fileSystemWatcher.Deleted -= OnDeleted;
|
||||||
|
fileSystemWatcher.Error -= OnError;
|
||||||
fileSystemWatcher.Dispose();
|
fileSystemWatcher.Dispose();
|
||||||
}
|
}
|
||||||
_fileWatchers.Clear();
|
_fileWatchers.Clear();
|
||||||
@ -165,13 +170,13 @@ public class LibraryWatcher : ILibraryWatcher
|
|||||||
{
|
{
|
||||||
if (e.ChangeType != WatcherChangeTypes.Changed) return;
|
if (e.ChangeType != WatcherChangeTypes.Changed) return;
|
||||||
_logger.LogDebug("[LibraryWatcher] Changed: {FullPath}, {Name}", e.FullPath, e.Name);
|
_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)
|
private void OnCreated(object sender, FileSystemEventArgs e)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("[LibraryWatcher] Created: {FullPath}, {Name}", e.FullPath, e.Name);
|
_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>
|
/// <summary>
|
||||||
@ -183,7 +188,7 @@ public class LibraryWatcher : ILibraryWatcher
|
|||||||
var isDirectory = string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name));
|
var isDirectory = string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name));
|
||||||
if (!isDirectory) return;
|
if (!isDirectory) return;
|
||||||
_logger.LogDebug("[LibraryWatcher] Deleted: {FullPath}, {Name}", e.FullPath, e.Name);
|
_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.
|
/// Processes the file or folder change. If the change is a file change and not from a supported extension, it will be ignored.
|
||||||
/// </summary>
|
/// </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 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="filePath">File or folder that changed</param>
|
||||||
/// <param name="isDirectoryChange">If the change is on a directory and not a file</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();
|
var sw = Stopwatch.StartNew();
|
||||||
|
_logger.LogDebug("[LibraryWatcher] Processing change of {FilePath}", filePath);
|
||||||
try
|
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 &&
|
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);
|
// var libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
|
||||||
if (string.IsNullOrEmpty(parentDirectory)) return;
|
// .SelectMany(l => l.Folders)
|
||||||
|
// .Distinct()
|
||||||
|
// .Select(Parser.Parser.NormalizePath)
|
||||||
|
// .Where(_directoryService.Exists)
|
||||||
|
// .ToList();
|
||||||
|
|
||||||
// We need to find the library this creation belongs to
|
var fullPath = GetFolder(filePath, _libraryFolders);
|
||||||
// Multiple libraries can point to the same base folder. In this case, we need use FirstOrDefault
|
if (string.IsNullOrEmpty(fullPath))
|
||||||
var libraryFolder = _libraryFolders.FirstOrDefault(f => parentDirectory.Contains(f));
|
{
|
||||||
if (string.IsNullOrEmpty(libraryFolder)) return;
|
_logger.LogDebug("[LibraryWatcher] Change from {FilePath} could not find root level folder, ignoring change", filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var rootFolder = _directoryService.GetFoldersTillRoot(libraryFolder, filePath).ToList();
|
// Check if this task has already enqueued or is being processed, before enquing
|
||||||
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()));
|
|
||||||
|
|
||||||
var alreadyScheduled =
|
var alreadyScheduled =
|
||||||
TaskScheduler.HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", new object[] {fullPath});
|
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)
|
if (!alreadyScheduled)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("[LibraryWatcher] Scheduling ScanFolder for {Folder}", fullPath);
|
_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.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);
|
_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);
|
UpdateVolumes(series, parsedInfos);
|
||||||
series.Pages = series.Volumes.Sum(v => v.Pages);
|
series.Pages = series.Volumes.Sum(v => v.Pages);
|
||||||
@ -479,10 +480,10 @@ public class ProcessSeries : IProcessSeries
|
|||||||
var deletedVolumes = series.Volumes.Except(nonDeletedVolumes);
|
var deletedVolumes = series.Volumes.Except(nonDeletedVolumes);
|
||||||
foreach (var volume in deletedVolumes)
|
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))
|
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}",
|
"[ScannerService] Volume cleanup code was trying to remove a volume with a file still existing on disk. File: {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);
|
await Task.WhenAll(processTasks);
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
@ -21,6 +23,7 @@ public interface IStatsService
|
|||||||
{
|
{
|
||||||
Task Send();
|
Task Send();
|
||||||
Task<ServerInfoDto> GetServerInfo();
|
Task<ServerInfoDto> GetServerInfo();
|
||||||
|
Task SendCancellation();
|
||||||
}
|
}
|
||||||
public class StatsService : IStatsService
|
public class StatsService : IStatsService
|
||||||
{
|
{
|
||||||
@ -127,6 +130,10 @@ public class StatsService : IStatsService
|
|||||||
MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(),
|
MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(),
|
||||||
MaxVolumesInASeries = await MaxVolumesInASeries(),
|
MaxVolumesInASeries = await MaxVolumesInASeries(),
|
||||||
MaxChaptersInASeries = await MaxChaptersInASeries(),
|
MaxChaptersInASeries = await MaxChaptersInASeries(),
|
||||||
|
MangaReaderBackgroundColors = await AllMangaReaderBackgroundColors(),
|
||||||
|
MangaReaderPageSplittingModes = await AllMangaReaderPageSplitting(),
|
||||||
|
MangaReaderLayoutModes = await AllMangaReaderLayoutModes(),
|
||||||
|
FileFormats = AllFormats(),
|
||||||
};
|
};
|
||||||
|
|
||||||
var usersWithPref = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences)).ToList();
|
var usersWithPref = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences)).ToList();
|
||||||
@ -149,6 +156,39 @@ public class StatsService : IStatsService
|
|||||||
return serverInfo;
|
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()
|
private Task<bool> GetIfUsingSeriesRelationship()
|
||||||
{
|
{
|
||||||
return _context.SeriesRelation.AnyAsync();
|
return _context.SeriesRelation.AnyAsync();
|
||||||
@ -190,4 +230,35 @@ public class StatsService : IStatsService
|
|||||||
.SelectMany(v => v.Chapters)
|
.SelectMany(v => v.Chapters)
|
||||||
.Count());
|
.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
|
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" ]
|
ENTRYPOINT [ "/bin/bash" ]
|
||||||
CMD ["/entrypoint.sh"]
|
CMD ["/entrypoint.sh"]
|
||||||
|
@ -11,12 +11,6 @@ public static class Configuration
|
|||||||
{
|
{
|
||||||
public static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
|
public static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
|
||||||
|
|
||||||
public static string Branch
|
|
||||||
{
|
|
||||||
get => GetBranch(GetAppSettingFilename());
|
|
||||||
set => SetBranch(GetAppSettingFilename(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int Port
|
public static int Port
|
||||||
{
|
{
|
||||||
get => GetPort(GetAppSettingFilename());
|
get => GetPort(GetAppSettingFilename());
|
||||||
@ -146,42 +140,4 @@ public static class Configuration
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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;
|
convertBookmarkToWebP: boolean;
|
||||||
enableSwaggerUi: boolean;
|
enableSwaggerUi: boolean;
|
||||||
totalBackups: number;
|
totalBackups: number;
|
||||||
|
totalLogs: number;
|
||||||
enableFolderWatching: boolean;
|
enableFolderWatching: boolean;
|
||||||
}
|
}
|
||||||
|
@ -21,14 +21,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0 mb-2">
|
<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>
|
<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>
|
<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>
|
<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">
|
<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>
|
||||||
|
|
||||||
<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>
|
<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>
|
<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>
|
<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>
|
</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</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>
|
<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>
|
<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>
|
<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('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required]));
|
||||||
this.settingsForm.addControl('enableSwaggerUi', new FormControl(this.serverSettings.enableSwaggerUi, [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('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('enableFolderWatching', new FormControl(this.serverSettings.enableFolderWatching, [Validators.required]));
|
||||||
this.settingsForm.addControl('convertBookmarkToWebP', new FormControl(this.serverSettings.convertBookmarkToWebP, []));
|
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('emailServiceUrl')?.setValue(this.serverSettings.emailServiceUrl);
|
||||||
this.settingsForm.get('enableSwaggerUi')?.setValue(this.serverSettings.enableSwaggerUi);
|
this.settingsForm.get('enableSwaggerUi')?.setValue(this.serverSettings.enableSwaggerUi);
|
||||||
this.settingsForm.get('totalBackups')?.setValue(this.serverSettings.totalBackups);
|
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('enableFolderWatching')?.setValue(this.serverSettings.enableFolderWatching);
|
||||||
this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP);
|
this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP);
|
||||||
this.settingsForm.markAsPristine();
|
this.settingsForm.markAsPristine();
|
||||||
|
@ -3,7 +3,7 @@ import { Title } from '@angular/platform-browser';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { forkJoin, Subject } from 'rxjs';
|
import { catchError, forkJoin, of, Subject } from 'rxjs';
|
||||||
import { take, takeUntil } from 'rxjs/operators';
|
import { take, takeUntil } from 'rxjs/operators';
|
||||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||||
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
|
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.hasSpecials = detail.specials.length > 0;
|
||||||
this.specials = detail.specials;
|
this.specials = detail.specials;
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
readerMode: parseInt(modelSettings.readerMode, 10),
|
readerMode: parseInt(modelSettings.readerMode, 10),
|
||||||
layoutMode: parseInt(modelSettings.layoutMode, 10),
|
layoutMode: parseInt(modelSettings.layoutMode, 10),
|
||||||
showScreenHints: modelSettings.showScreenHints,
|
showScreenHints: modelSettings.showScreenHints,
|
||||||
backgroundColor: modelSettings.backgroundColor,
|
backgroundColor: this.user.preferences.backgroundColor,
|
||||||
bookReaderFontFamily: modelSettings.bookReaderFontFamily,
|
bookReaderFontFamily: modelSettings.bookReaderFontFamily,
|
||||||
bookReaderLineSpacing: modelSettings.bookReaderLineSpacing,
|
bookReaderLineSpacing: modelSettings.bookReaderLineSpacing,
|
||||||
bookReaderFontSize: modelSettings.bookReaderFontSize,
|
bookReaderFontSize: modelSettings.bookReaderFontSize,
|
||||||
|
@ -3,24 +3,7 @@
|
|||||||
if [ ! -f "/kavita/config/appsettings.json" ]; then
|
if [ ! -f "/kavita/config/appsettings.json" ]; then
|
||||||
echo "Kavita configuration file does not exist, creating..."
|
echo "Kavita configuration file does not exist, creating..."
|
||||||
echo '{
|
echo '{
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Data source=config//kavita.db"
|
|
||||||
},
|
|
||||||
"TokenKey": "super secret unguessable key",
|
"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
|
"Port": 5000
|
||||||
}' >> /kavita/config/appsettings.json
|
}' >> /kavita/config/appsettings.json
|
||||||
@ -28,4 +11,4 @@ fi
|
|||||||
|
|
||||||
chmod +x Kavita
|
chmod +x Kavita
|
||||||
|
|
||||||
./Kavita
|
./Kavita
|
||||||
|
Loading…
x
Reference in New Issue
Block a user