diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index 24dedef47..ccae1a835 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -37,12 +37,14 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc public async Task>> GetAllGenres(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); + + // NOTE: libraryIds isn't hooked up in the frontend if (ids is {Count: > 0}) { - return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, User.GetUserId())); + return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(User.GetUserId(), ids)); } - return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosAsync(User.GetUserId())); + return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(User.GetUserId())); } /// @@ -71,9 +73,9 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); if (ids is {Count: > 0}) { - return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, User.GetUserId())); + return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(User.GetUserId(), ids)); } - return Ok(await unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId())); + return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(User.GetUserId())); } /// @@ -88,9 +90,9 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); if (ids is {Count: > 0}) { - return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, User.GetUserId())); + return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(User.GetUserId(), ids)); } - return Ok(await unitOfWork.TagRepository.GetAllTagDtosAsync(User.GetUserId())); + return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(User.GetUserId())); } /// diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index 94e3c8e7f..a68c3c548 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -19,9 +19,8 @@ public interface IGenreRepository Task FindByNameAsync(string genreName); Task> GetAllGenresAsync(); Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames); - Task> GetAllGenreDtosAsync(int userId); Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false); - Task> GetAllGenreDtosForLibrariesAsync(IList libraryIds, int userId); + Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null); Task GetCountAsync(); Task GetRandomGenre(); Task GetGenreById(int id); @@ -69,27 +68,6 @@ public class GenreRepository : IGenreRepository await _context.SaveChangesAsync(); } - /// - /// Returns a set of Genre tags for a set of library Ids. UserId will restrict returned Genres based on user's age restriction. - /// - /// - /// - /// - public async Task> GetAllGenreDtosForLibrariesAsync(IList libraryIds, int userId) - { - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.Series - .Where(s => libraryIds.Contains(s.LibraryId)) - .RestrictAgainstAgeRestriction(userRating) - .SelectMany(s => s.Metadata.Genres) - .AsSplitQuery() - .Distinct() - .OrderBy(p => p.NormalizedTitle) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task GetCountAsync() { return await _context.Genre.CountAsync(); @@ -128,13 +106,30 @@ public class GenreRepository : IGenreRepository .ToListAsync(); } - public async Task> GetAllGenreDtosAsync(int userId) + /// + /// Returns a set of Genre tags for a set of library Ids. + /// UserId will restrict returned Genres based on user's age restriction and library access. + /// + /// + /// + /// + public async Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.Genre - .RestrictAgainstAgeRestriction(ageRating) - .OrderBy(g => g.NormalizedTitle) - .AsNoTracking() + var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + + if (libraryIds is {Count: > 0}) + { + userLibs = userLibs.Where(libraryIds.Contains).ToList(); + } + + return await _context.Series + .Where(s => userLibs.Contains(s.LibraryId)) + .RestrictAgainstAgeRestriction(userRating) + .SelectMany(s => s.Metadata.Genres) + .AsSplitQuery() + .Distinct() + .OrderBy(p => p.NormalizedTitle) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index 1c9e71e1e..5633d7403 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -20,7 +20,7 @@ public interface IPersonRepository Task> GetAllPersonDtosAsync(int userId); Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role); Task RemoveAllPeopleNoLongerAssociated(); - Task> GetAllPeopleDtosForLibrariesAsync(List libraryIds, int userId); + Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null); Task GetCountAsync(); Task> GetAllPeopleByRoleAndNames(PersonRole role, IEnumerable normalizeNames); @@ -61,11 +61,18 @@ public class PersonRepository : IPersonRepository await _context.SaveChangesAsync(); } - public async Task> GetAllPeopleDtosForLibrariesAsync(List libraryIds, int userId) + public async Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + + if (libraryIds is {Count: > 0}) + { + userLibs = userLibs.Where(libraryIds.Contains).ToList(); + } + return await _context.Series - .Where(s => libraryIds.Contains(s.LibraryId)) + .Where(s => userLibs.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(ageRating) .SelectMany(s => s.Metadata.People) .Distinct() @@ -99,6 +106,7 @@ public class PersonRepository : IPersonRepository public async Task> GetAllPersonDtosAsync(int userId) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); return await _context.Person .OrderBy(p => p.Name) .RestrictAgainstAgeRestriction(ageRating) diff --git a/API/Data/Repositories/TagRepository.cs b/API/Data/Repositories/TagRepository.cs index 4423c09e3..2fdb8377e 100644 --- a/API/Data/Repositories/TagRepository.cs +++ b/API/Data/Repositories/TagRepository.cs @@ -19,7 +19,7 @@ public interface ITagRepository Task> GetAllTagsByNameAsync(IEnumerable normalizedNames); Task> GetAllTagDtosAsync(int userId); Task RemoveAllTagNoLongerAssociated(); - Task> GetAllTagDtosForLibrariesAsync(IList libraryIds, int userId); + Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null); } public class TagRepository : ITagRepository @@ -57,11 +57,18 @@ public class TagRepository : ITagRepository await _context.SaveChangesAsync(); } - public async Task> GetAllTagDtosForLibrariesAsync(IList libraryIds, int userId) + public async Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null) { var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + + if (libraryIds is {Count: > 0}) + { + userLibs = userLibs.Where(libraryIds.Contains).ToList(); + } + return await _context.Series - .Where(s => libraryIds.Contains(s.LibraryId)) + .Where(s => userLibs.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .SelectMany(s => s.Metadata.Tags) .AsSplitQuery() diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 5a38c5903..8c6c796c9 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -605,7 +605,7 @@ public class DirectoryService : IDirectoryService { if (!file.Contains(folder)) continue; - var lowestPath = Path.GetDirectoryName(file)?.Replace(folder, string.Empty); + var lowestPath = Path.GetDirectoryName(file); if (!string.IsNullOrEmpty(lowestPath)) { dirs.TryAdd(Parser.NormalizePath(lowestPath), string.Empty); diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 6c4946307..6f6ba9336 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -137,19 +137,6 @@ public class ParseScannedFiles await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent(directory, library.Name, ProgressEventType.Updated)); - // This is debug code to help understand why some installs aren't working correctly - if (!forceCheck && seriesPaths.TryGetValue(directory, out var series2) && series2.Count > 1 && series2.All(s => !string.IsNullOrEmpty(s.LowestFolderPath))) - { - _logger.LogDebug("[ProcessFiles] Dirty check passed, series list: {@SeriesModified}", series2); - foreach (var s in series2) - { - _logger.LogDebug("[ProcessFiles] Last Scanned: {LastScanned} vs Directory Check: {DirectoryLastScanned}", - s.LastScanned, _directoryService - .GetLastWriteTime(s.LowestFolderPath!) - .Truncate(TimeSpan.TicksPerSecond)); - } - - } if (HasSeriesFolderNotChangedSinceLastScan(seriesPaths, directory, forceCheck)) { @@ -177,12 +164,12 @@ public class ParseScannedFiles if (!hasFolderChangedSinceLastScan) { - _logger.LogDebug("[ProcessFiles] {Directory} subfolder {Folder} did not change since last scan, adding entry to skip", directory, seriesModified.LowestFolderPath); + _logger.LogTrace("[ProcessFiles] {Directory} subfolder {Folder} did not change since last scan, adding entry to skip", directory, seriesModified.LowestFolderPath); result.Add(CreateScanResult(seriesModified.LowestFolderPath!, folderPath, false, ArraySegment.Empty)); } else { - _logger.LogDebug("[ProcessFiles] {Directory} subfolder {Folder} changed for Series {SeriesName}", directory, seriesModified.LowestFolderPath, seriesModified.SeriesName); + _logger.LogTrace("[ProcessFiles] {Directory} subfolder {Folder} changed for Series {SeriesName}", directory, seriesModified.LowestFolderPath, seriesModified.SeriesName); result.Add(CreateScanResult(directory, folderPath, true, _directoryService.ScanFiles(seriesModified.LowestFolderPath!, fileExtensions, matcher))); } @@ -190,7 +177,6 @@ public class ParseScannedFiles } else { - // For a scan, this is doing everything in the directory loop before the folder Action is called...which leads to no progress indication result.Add(CreateScanResult(directory, folderPath, true, _directoryService.ScanFiles(directory, fileExtensions, matcher))); } diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 23f703f86..452f36748 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -355,44 +355,7 @@ public class ProcessSeries : IProcessSeries // Set the AgeRating as highest in all the comicInfos if (!series.Metadata.AgeRatingLocked) series.Metadata.AgeRating = chapters.Max(chapter => chapter.AgeRating); - // Count (aka expected total number of chapters or volumes from metadata) across all chapters - series.Metadata.TotalCount = chapters.Max(chapter => chapter.TotalCount); - // The actual number of count's defined across all chapter's metadata - series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count); - - var maxVolume = (int) series.Volumes.Max(v => v.MaxNumber); - var maxChapter = (int) chapters.Max(c => c.MaxNumber); - - // Single books usually don't have a number in their Range (filename) - if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1) - { - series.Metadata.MaxCount = 1; - } else if (series.Metadata.TotalCount <= 1 && chapters.Count == 1 && chapters[0].IsSpecial) - { - // If a series has a TotalCount of 1 (or no total count) and there is only a Special, mark it as Complete - series.Metadata.MaxCount = series.Metadata.TotalCount; - } else if ((maxChapter == Parser.Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) && maxVolume <= series.Metadata.TotalCount) - { - series.Metadata.MaxCount = maxVolume; - } else if (maxVolume == series.Metadata.TotalCount) - { - series.Metadata.MaxCount = maxVolume; - } else - { - series.Metadata.MaxCount = maxChapter; - } - - if (!series.Metadata.PublicationStatusLocked) - { - series.Metadata.PublicationStatus = PublicationStatus.OnGoing; - if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0) - { - series.Metadata.PublicationStatus = PublicationStatus.Completed; - } else if (series.Metadata.TotalCount > 0 && series.Metadata.MaxCount > 0) - { - series.Metadata.PublicationStatus = PublicationStatus.Ended; - } - } + DeterminePublicationStatus(series, chapters); if (!string.IsNullOrEmpty(firstChapter?.Summary) && !series.Metadata.SummaryLocked) { @@ -616,6 +579,48 @@ public class ProcessSeries : IProcessSeries } + private static void DeterminePublicationStatus(Series series, List chapters) + { + // Count (aka expected total number of chapters or volumes from metadata) across all chapters + series.Metadata.TotalCount = chapters.Max(chapter => chapter.TotalCount); + // The actual number of count's defined across all chapter's metadata + series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count); + + var maxVolume = (int) series.Volumes.Where(v => v.MaxNumber.IsNot(Parser.Parser.SpecialVolumeNumber)).Max(v => v.MaxNumber); + var maxChapter = (int) chapters.Max(c => c.MaxNumber); + + // Single books usually don't have a number in their Range (filename) + if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1) + { + series.Metadata.MaxCount = 1; + } else if (series.Metadata.TotalCount <= 1 && chapters.Count == 1 && chapters[0].IsSpecial) + { + // If a series has a TotalCount of 1 (or no total count) and there is only a Special, mark it as Complete + series.Metadata.MaxCount = series.Metadata.TotalCount; + } else if ((maxChapter == Parser.Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) && maxVolume <= series.Metadata.TotalCount) + { + series.Metadata.MaxCount = maxVolume; + } else if (maxVolume == series.Metadata.TotalCount) + { + series.Metadata.MaxCount = maxVolume; + } else + { + series.Metadata.MaxCount = maxChapter; + } + + if (!series.Metadata.PublicationStatusLocked) + { + series.Metadata.PublicationStatus = PublicationStatus.OnGoing; + if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0) + { + series.Metadata.PublicationStatus = PublicationStatus.Completed; + } else if (series.Metadata.TotalCount > 0 && series.Metadata.MaxCount > 0) + { + series.Metadata.PublicationStatus = PublicationStatus.Ended; + } + } + } + private async Task UpdateVolumes(Series series, IList parsedInfos, bool forceUpdate = false) { // Add new volumes and update chapters per volume diff --git a/UI/Web/src/app/library-detail/library-detail.component.ts b/UI/Web/src/app/library-detail/library-detail.component.ts index c0972182d..d672e83c7 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -241,7 +241,6 @@ export class LibraryDetailComponent implements OnInit { async handleAction(action: ActionItem, library: Library) { let lib: Partial = library; if (library === undefined) { - //lib = {id: this.libraryId, name: this.libraryName}; // BUG: We need the whole library for editLibrary this.libraryService.getLibrary(this.libraryId).subscribe(async library => { switch (action.action) { case(Action.Scan):