mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Shakeout Fixes (#422)
# Fixed - Fixed: Clean the pdf extension from Series title for PDF types - Fixed: Fixed a bug where a forced metadata refresh wouldn't trigger a volume to force a refresh of cover image - Fixed: Fixed an issue where Removing series no longer on disk would not use the Series Format and thus after deleting files, they would not be removed. - Fixed: Fixed an issue with reading a single image file, where the cache code would not properly move the file - Fixed: For Specials, Get Next/Prev Chapter should use the filename instead of arbitrary Number (which is always 0). Use the same sorting logic when requesting volumes on series detail, so sorting can happen in the backend. # Added - Added: (Accessibility) Nearly every page has had a title set for it =============================================================================== * Clean the pdf extension from ParseSeries * Fixed a bug where forced metadata refresh wouldn't trigger the volume to update it's image. * Added titles to most pages to help distinguish back/forward history. Fixed a bug in the scanner which didn't account for Format when calculating if we need to remove a series not on disk. * For Specials, Get Next/Prev Chapter should use the filename instead of arbitrary Number (which is always 0). Use the same sorting logic when requesting volumes on series detail, so sorting can happen in the backend. * Fixed unit tests
This commit is contained in:
parent
29edadb506
commit
ebd4ec25bf
@ -11,4 +11,4 @@ namespace API.Tests.Parser
|
||||
Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ namespace API.Tests.Services
|
||||
IUnitOfWork unitOfWork = new UnitOfWork(_context, Substitute.For<IMapper>(), null);
|
||||
|
||||
|
||||
IMetadataService metadataService = Substitute.For<MetadataService>(unitOfWork, _metadataLogger, _archiveService, _bookService, _directoryService, _imageService);
|
||||
IMetadataService metadataService = Substitute.For<MetadataService>(unitOfWork, _metadataLogger, _archiveService, _bookService, _imageService);
|
||||
_scannerService = new ScannerService(unitOfWork, _logger, _archiveService, metadataService, _bookService);
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ namespace API.Controllers
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||
private readonly NaturalSortComparer _naturalSortComparer = new NaturalSortComparer();
|
||||
|
||||
public ReaderController(IDirectoryService directoryService, ICacheService cacheService, IUnitOfWork unitOfWork)
|
||||
{
|
||||
@ -295,8 +296,8 @@ namespace API.Controllers
|
||||
var currentChapter = await _unitOfWork.VolumeRepository.GetChapterAsync(currentChapterId);
|
||||
if (currentVolume.Number == 0)
|
||||
{
|
||||
// Handle specials
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer), currentChapter.Number);
|
||||
// Handle specials by sorting on their Filename aka Range
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Range, _naturalSortComparer), currentChapter.Number);
|
||||
if (chapterId > 0) return Ok(chapterId);
|
||||
}
|
||||
|
||||
@ -362,7 +363,7 @@ namespace API.Controllers
|
||||
|
||||
if (currentVolume.Number == 0)
|
||||
{
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer).Reverse(), currentChapter.Number);
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Range, _naturalSortComparer).Reverse(), currentChapter.Number);
|
||||
if (chapterId > 0) return Ok(chapterId);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
@ -16,7 +18,7 @@ namespace API.Data
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
private readonly NaturalSortComparer _naturalSortComparer = new ();
|
||||
public SeriesRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
@ -37,7 +39,7 @@ namespace API.Data
|
||||
{
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
|
||||
public bool SaveAll()
|
||||
{
|
||||
return _context.SaveChanges() > 0;
|
||||
@ -60,12 +62,12 @@ namespace API.Data
|
||||
.Where(s => libraries.Contains(s.LibraryId) && s.Name == name)
|
||||
.CountAsync() > 1;
|
||||
}
|
||||
|
||||
|
||||
public Series GetSeriesByName(string name)
|
||||
{
|
||||
return _context.Series.SingleOrDefault(x => x.Name == name);
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId)
|
||||
{
|
||||
return await _context.Series
|
||||
@ -73,7 +75,7 @@ namespace API.Data
|
||||
.OrderBy(s => s.SortName)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams)
|
||||
{
|
||||
var query = _context.Series
|
||||
@ -84,12 +86,12 @@ namespace API.Data
|
||||
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<SearchResultDto>> SearchSeries(int[] libraryIds, string searchQuery)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%")
|
||||
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%"))
|
||||
.Include(s => s.Library)
|
||||
@ -108,12 +110,23 @@ namespace API.Data
|
||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
await AddVolumeModifiers(userId, volumes);
|
||||
SortSpecialChapters(volumes);
|
||||
|
||||
|
||||
|
||||
return volumes;
|
||||
}
|
||||
|
||||
private void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
||||
{
|
||||
foreach (var v in volumes.Where(vdto => vdto.Number == 0))
|
||||
{
|
||||
v.Chapters = v.Chapters.OrderBy(x => x.Range, _naturalSortComparer).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<Volume>> GetVolumes(int seriesId)
|
||||
{
|
||||
@ -133,7 +146,7 @@ namespace API.Data
|
||||
|
||||
var seriesList = new List<SeriesDto>() {series};
|
||||
await AddSeriesModifiers(userId, seriesList);
|
||||
|
||||
|
||||
return seriesList[0];
|
||||
}
|
||||
|
||||
@ -152,7 +165,7 @@ namespace API.Data
|
||||
.AsNoTracking()
|
||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||
.SingleAsync();
|
||||
|
||||
|
||||
}
|
||||
|
||||
public async Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId)
|
||||
@ -163,7 +176,7 @@ namespace API.Data
|
||||
.ThenInclude(c => c.Files)
|
||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||
.SingleAsync(vol => vol.Id == volumeId);
|
||||
|
||||
|
||||
var volumeList = new List<VolumeDto>() {volume};
|
||||
await AddVolumeModifiers(userId, volumeList);
|
||||
|
||||
@ -186,7 +199,7 @@ namespace API.Data
|
||||
{
|
||||
var series = await _context.Series.Where(s => s.Id == seriesId).SingleOrDefaultAsync();
|
||||
_context.Series.Remove(series);
|
||||
|
||||
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
@ -212,7 +225,7 @@ namespace API.Data
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
IList<int> chapterIds = new List<int>();
|
||||
foreach (var s in series)
|
||||
{
|
||||
@ -266,7 +279,7 @@ namespace API.Data
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
private async Task AddVolumeModifiers(int userId, List<VolumeDto> volumes)
|
||||
private async Task AddVolumeModifiers(int userId, IReadOnlyCollection<VolumeDto> volumes)
|
||||
{
|
||||
var userProgress = await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId && volumes.Select(s => s.Id).Contains(p.VolumeId))
|
||||
@ -279,7 +292,7 @@ namespace API.Data
|
||||
{
|
||||
c.PagesRead = userProgress.Where(p => p.ChapterId == c.Id).Sum(p => p.PagesRead);
|
||||
}
|
||||
|
||||
|
||||
v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead);
|
||||
}
|
||||
}
|
||||
@ -311,7 +324,7 @@ namespace API.Data
|
||||
|
||||
return await PagedList<SeriesDto>.CreateAsync(allQuery, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => s.LibraryId == libraryId)
|
||||
.AsNoTracking()
|
||||
@ -356,7 +369,7 @@ namespace API.Data
|
||||
{
|
||||
series = series.Where(s => s.AppUserId == userId
|
||||
&& s.PagesRead > 0
|
||||
&& s.PagesRead < s.Series.Pages
|
||||
&& s.PagesRead < s.Series.Pages
|
||||
&& s.Series.LibraryId == libraryId);
|
||||
}
|
||||
var retSeries = await series
|
||||
@ -365,7 +378,7 @@ namespace API.Data
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
return retSeries.DistinctBy(s => s.Name).Take(limit);
|
||||
}
|
||||
|
||||
@ -386,7 +399,7 @@ namespace API.Data
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
return metadataDto;
|
||||
}
|
||||
|
||||
@ -398,7 +411,7 @@ namespace API.Data
|
||||
.AsNoTracking()
|
||||
.Select(library => library.Id)
|
||||
.ToList();
|
||||
|
||||
|
||||
var query = _context.CollectionTag
|
||||
.Where(s => s.Id == collectionId)
|
||||
.Include(c => c.SeriesMetadatas)
|
||||
@ -423,4 +436,4 @@ namespace API.Data
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Parser;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner;
|
||||
|
||||
namespace API.Extensions
|
||||
|
@ -498,6 +498,12 @@ namespace API.Parser
|
||||
ret.Series = CleanTitle(fileName);
|
||||
}
|
||||
|
||||
// Pdfs may have .pdf in the series name, remove that
|
||||
if (IsPdf(fileName) && ret.Series.ToLower().EndsWith(".pdf"))
|
||||
{
|
||||
ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length);
|
||||
}
|
||||
|
||||
return ret.Series == string.Empty ? null : ret;
|
||||
}
|
||||
|
||||
|
@ -60,8 +60,15 @@ namespace API.Services
|
||||
if (files.Count > 0 && files[0].Format == MangaFormat.Image)
|
||||
{
|
||||
DirectoryService.ExistOrCreate(extractPath);
|
||||
var pattern = (files.Count == 1) ? (@"\" + Path.GetExtension(files[0].FilePath)) : Parser.Parser.ImageFileExtensions;
|
||||
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, pattern);
|
||||
if (files.Count == 1)
|
||||
{
|
||||
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, Parser.Parser.ImageFileExtensions);
|
||||
}
|
||||
|
||||
extractDi.Flatten();
|
||||
return chapter;
|
||||
}
|
||||
|
@ -20,19 +20,17 @@ namespace API.Services
|
||||
private readonly ILogger<MetadataService> _logger;
|
||||
private readonly IArchiveService _archiveService;
|
||||
private readonly IBookService _bookService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||
public static readonly int ThumbnailWidth = 320; // 153w x 230h
|
||||
|
||||
public MetadataService(IUnitOfWork unitOfWork, ILogger<MetadataService> logger,
|
||||
IArchiveService archiveService, IBookService bookService, IDirectoryService directoryService, IImageService imageService)
|
||||
IArchiveService archiveService, IBookService bookService, IImageService imageService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
_archiveService = archiveService;
|
||||
_bookService = bookService;
|
||||
_directoryService = directoryService;
|
||||
_imageService = imageService;
|
||||
}
|
||||
|
||||
@ -71,25 +69,24 @@ namespace API.Services
|
||||
|
||||
public void UpdateMetadata(Volume volume, bool forceUpdate)
|
||||
{
|
||||
if (volume != null && ShouldFindCoverImage(volume.CoverImage, forceUpdate))
|
||||
{
|
||||
volume.Chapters ??= new List<Chapter>();
|
||||
var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer).FirstOrDefault();
|
||||
if (volume == null || !ShouldFindCoverImage(volume.CoverImage, forceUpdate)) return;
|
||||
|
||||
// Skip calculating Cover Image (I/O) if the chapter already has it set
|
||||
if (firstChapter == null || ShouldFindCoverImage(firstChapter.CoverImage))
|
||||
volume.Chapters ??= new List<Chapter>();
|
||||
var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer).FirstOrDefault();
|
||||
|
||||
// Skip calculating Cover Image (I/O) if the chapter already has it set
|
||||
if (firstChapter == null || ShouldFindCoverImage(firstChapter.CoverImage, forceUpdate))
|
||||
{
|
||||
var firstFile = firstChapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
||||
if (firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified))
|
||||
{
|
||||
var firstFile = firstChapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
||||
if (firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified))
|
||||
{
|
||||
volume.CoverImage = GetCoverImage(firstFile);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
volume.CoverImage = firstChapter.CoverImage;
|
||||
volume.CoverImage = GetCoverImage(firstFile);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
volume.CoverImage = firstChapter.CoverImage;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateMetadata(Series series, bool forceUpdate)
|
||||
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Interfaces.Services;
|
||||
using API.Parser;
|
||||
@ -38,6 +39,20 @@ namespace API.Services.Tasks.Scanner
|
||||
_scannedSeries = new ConcurrentDictionary<ParsedSeries, List<ParserInfo>>();
|
||||
}
|
||||
|
||||
public static IList<ParserInfo> GetInfosByName(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series)
|
||||
{
|
||||
var existingKey = parsedSeries.Keys.FirstOrDefault(ps =>
|
||||
ps.Format == series.Format && ps.NormalizedName == Parser.Parser.Normalize(series.OriginalName));
|
||||
existingKey ??= new ParsedSeries()
|
||||
{
|
||||
Format = series.Format,
|
||||
Name = series.OriginalName,
|
||||
NormalizedName = Parser.Parser.Normalize(series.OriginalName)
|
||||
};
|
||||
|
||||
return parsedSeries[existingKey];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes files found during a library scan.
|
||||
/// Populates a collection of <see cref="ParserInfo"/> for DB updates later.
|
||||
@ -144,7 +159,7 @@ namespace API.Services.Tasks.Scanner
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
totalFiles = 0;
|
||||
var searchPattern = GetLibrarySearchPattern(libraryType);
|
||||
var searchPattern = GetLibrarySearchPattern();
|
||||
foreach (var folderPath in folders)
|
||||
{
|
||||
try
|
||||
@ -174,12 +189,7 @@ namespace API.Services.Tasks.Scanner
|
||||
return SeriesWithInfos();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given the Library Type, returns the regex pattern that restricts which files types will be found during a file scan.
|
||||
/// </summary>
|
||||
/// <param name="libraryType"></param>
|
||||
/// <returns></returns>
|
||||
private static string GetLibrarySearchPattern(LibraryType libraryType)
|
||||
private static string GetLibrarySearchPattern()
|
||||
{
|
||||
return Parser.Parser.SupportedExtensions;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@ -27,7 +26,7 @@ namespace API.Services.Tasks
|
||||
private readonly IArchiveService _archiveService;
|
||||
private readonly IMetadataService _metadataService;
|
||||
private readonly IBookService _bookService;
|
||||
private readonly NaturalSortComparer _naturalSort;
|
||||
private readonly NaturalSortComparer _naturalSort = new ();
|
||||
|
||||
public ScannerService(IUnitOfWork unitOfWork, ILogger<ScannerService> logger, IArchiveService archiveService,
|
||||
IMetadataService metadataService, IBookService bookService)
|
||||
@ -37,7 +36,6 @@ namespace API.Services.Tasks
|
||||
_archiveService = archiveService;
|
||||
_metadataService = metadataService;
|
||||
_bookService = bookService;
|
||||
_naturalSort = new NaturalSortComparer();
|
||||
}
|
||||
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 360)]
|
||||
@ -200,10 +198,16 @@ namespace API.Services.Tasks
|
||||
_logger.LogInformation("Removed {RemoveMissingSeries} series that are no longer on disk:", removeCount);
|
||||
foreach (var s in missingSeries)
|
||||
{
|
||||
_logger.LogDebug("Removed {SeriesName}", s.Name);
|
||||
_logger.LogDebug("Removed {SeriesName} ({Format})", s.Name, s.Format);
|
||||
}
|
||||
}
|
||||
|
||||
if (library.Series.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("Removed all Series, returning without checking reset of files scanned");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Add new series that have parsedInfos
|
||||
foreach (var (key, infos) in parsedSeries)
|
||||
@ -218,11 +222,11 @@ namespace API.Services.Tasks
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogCritical(e, "There are multiple series that map to normalized key {Key}. You can manually delete the entity via UI and rescan to fix it", key);
|
||||
_logger.LogCritical(e, "There are multiple series that map to normalized key {Key}. You can manually delete the entity via UI and rescan to fix it", key.NormalizedName);
|
||||
var duplicateSeries = library.Series.Where(s => s.NormalizedName == key.NormalizedName || Parser.Parser.Normalize(s.OriginalName) == key.NormalizedName).ToList();
|
||||
foreach (var series in duplicateSeries)
|
||||
{
|
||||
_logger.LogCritical("{Key} maps with {Series}", key, series.OriginalName);
|
||||
_logger.LogCritical("{Key} maps with {Series}", key.Name, series.OriginalName);
|
||||
}
|
||||
|
||||
continue;
|
||||
@ -247,7 +251,7 @@ namespace API.Services.Tasks
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Processing series {SeriesName}", series.OriginalName);
|
||||
UpdateVolumes(series, GetInfosByName(parsedSeries, series).ToArray());
|
||||
UpdateVolumes(series, ParseScannedFiles.GetInfosByName(parsedSeries, series).ToArray());
|
||||
series.Pages = series.Volumes.Sum(v => v.Pages);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -257,25 +261,15 @@ namespace API.Services.Tasks
|
||||
});
|
||||
}
|
||||
|
||||
private static IList<ParserInfo> GetInfosByName(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series)
|
||||
{
|
||||
// TODO: Move this into a common place
|
||||
var existingKey = parsedSeries.Keys.FirstOrDefault(ps =>
|
||||
ps.Format == series.Format && ps.NormalizedName == Parser.Parser.Normalize(series.OriginalName));
|
||||
existingKey ??= new ParsedSeries()
|
||||
{
|
||||
Format = series.Format,
|
||||
Name = series.OriginalName,
|
||||
NormalizedName = Parser.Parser.Normalize(series.OriginalName)
|
||||
};
|
||||
|
||||
return parsedSeries[existingKey];
|
||||
}
|
||||
|
||||
public IEnumerable<Series> FindSeriesNotOnDisk(ICollection<Series> existingSeries, Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries)
|
||||
{
|
||||
var foundSeries = parsedSeries.Select(s => s.Key.Name).ToList();
|
||||
return existingSeries.Where(es => !es.NameInList(foundSeries));
|
||||
// It is safe to check only first since Parser ensures that a Series only has one type
|
||||
var format = MangaFormat.Unknown;
|
||||
var firstPs = parsedSeries.Keys.DistinctBy(ps => ps.Format).FirstOrDefault();
|
||||
if (firstPs != null) format = firstPs.Format;
|
||||
|
||||
var foundSeries = parsedSeries.Select(s => s.Key.Name).ToList();
|
||||
return existingSeries.Where(es => !es.NameInList(foundSeries) || es.Format != format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -293,7 +287,7 @@ namespace API.Services.Tasks
|
||||
|
||||
existingSeries = existingSeries.Where(
|
||||
s => !missingList.Exists(
|
||||
m => m.NormalizedName.Equals(s.NormalizedName))).ToList();
|
||||
m => m.NormalizedName.Equals(s.NormalizedName) && m.Format == s.Format)).ToList();
|
||||
|
||||
removeCount = existingCount - existingSeries.Count;
|
||||
|
||||
|
@ -18,4 +18,4 @@ namespace Kavita.Common
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export class AuthGuard implements CanActivate {
|
||||
}
|
||||
this.toastr.error('You are not authorized to view this page.');
|
||||
localStorage.setItem(this.urlKey, window.location.pathname);
|
||||
this.router.navigateByUrl('/home');
|
||||
this.router.navigateByUrl('/libraries');
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
|
||||
|
||||
@ -22,7 +23,7 @@ export class DashboardComponent implements OnInit {
|
||||
counter = this.tabs.length + 1;
|
||||
active = this.tabs[0];
|
||||
|
||||
constructor(public route: ActivatedRoute, private serverService: ServerService, private toastr: ToastrService) {
|
||||
constructor(public route: ActivatedRoute, private serverService: ServerService, private toastr: ToastrService, private titleService: Title) {
|
||||
this.route.fragment.subscribe(frag => {
|
||||
const tab = this.tabs.filter(item => item.fragment === frag);
|
||||
if (tab.length > 0) {
|
||||
@ -34,7 +35,9 @@ export class DashboardComponent implements OnInit {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
ngOnInit() {
|
||||
this.titleService.setTitle('Kavita - Admin Dashboard');
|
||||
}
|
||||
|
||||
restartServer() {
|
||||
this.serverService.restart().subscribe(() => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
@ -28,7 +29,9 @@ export class AllCollectionsComponent implements OnInit {
|
||||
seriesPagination!: Pagination;
|
||||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
||||
|
||||
constructor(private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService, private modalService: NgbModal) {
|
||||
constructor(private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute,
|
||||
private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService,
|
||||
private modalService: NgbModal, private titleService: Title) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
|
||||
const routeId = this.route.snapshot.paramMap.get('id');
|
||||
@ -43,6 +46,7 @@ export class AllCollectionsComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
this.collectionTagName = tags.filter(item => item.id === this.collectionTagId)[0].title;
|
||||
this.titleService.setTitle('Kavita - ' + this.collectionTagName + ' Collection');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { Router } from '@angular/router';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { MemberService } from '../_services/member.service';
|
||||
import { AccountService } from '../_services/account.service';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
@ -19,7 +20,7 @@ export class HomeComponent implements OnInit {
|
||||
password: new FormControl('', [Validators.required])
|
||||
});
|
||||
|
||||
constructor(public accountService: AccountService, private memberService: MemberService, private router: Router) {
|
||||
constructor(public accountService: AccountService, private memberService: MemberService, private router: Router, private titleService: Title) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -31,6 +32,7 @@ export class HomeComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.titleService.setTitle('Kavita');
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.router.navigateByUrl('/library');
|
||||
|
@ -28,7 +28,7 @@ export class LibraryDetailComponent implements OnInit {
|
||||
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService, private actionService: ActionService) {
|
||||
const routeId = this.route.snapshot.paramMap.get('id');
|
||||
if (routeId === null) {
|
||||
this.router.navigateByUrl('/home');
|
||||
this.router.navigateByUrl('/libraries');
|
||||
return;
|
||||
}
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { take } from 'rxjs/operators';
|
||||
@ -34,9 +35,13 @@ export class LibraryComponent implements OnInit {
|
||||
|
||||
seriesTrackBy = (index: number, item: any) => `${item.name}_${item.pagesRead}`;
|
||||
|
||||
constructor(public accountService: AccountService, private libraryService: LibraryService, private seriesService: SeriesService, private actionFactoryService: ActionFactoryService, private collectionService: CollectionTagService, private router: Router, private modalService: NgbModal) { }
|
||||
constructor(public accountService: AccountService, private libraryService: LibraryService,
|
||||
private seriesService: SeriesService, private actionFactoryService: ActionFactoryService,
|
||||
private collectionService: CollectionTagService, private router: Router,
|
||||
private modalService: NgbModal, private titleService: Title) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.titleService.setTitle('Kavita - Dashboard');
|
||||
this.isLoading = true;
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
this.user = user;
|
||||
|
@ -245,7 +245,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const chapterId = this.route.snapshot.paramMap.get('chapterId');
|
||||
|
||||
if (libraryId === null || seriesId === null || chapterId === null) {
|
||||
this.router.navigateByUrl('/home');
|
||||
this.router.navigateByUrl('/libraries');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
||||
logout() {
|
||||
this.accountService.logout();
|
||||
this.navService.hideNavBar();
|
||||
this.router.navigateByUrl('/home');
|
||||
this.router.navigateByUrl('/login');
|
||||
}
|
||||
|
||||
moveFocus() {
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config';
|
||||
import { ConfirmService } from '../shared/confirm.service';
|
||||
import { CardDetailsModalComponent } from '../shared/_modals/card-details-modal/card-details-modal.component';
|
||||
import { DownloadService } from '../shared/_services/download.service';
|
||||
import { NaturalSortService } from '../shared/_services/natural-sort.service';
|
||||
import { UtilityService } from '../shared/_services/utility.service';
|
||||
import { EditSeriesModalComponent } from '../_modals/edit-series-modal/edit-series-modal.component';
|
||||
import { ReviewSeriesModalComponent } from '../_modals/review-series-modal/review-series-modal.component';
|
||||
@ -81,7 +80,7 @@ export class SeriesDetailComponent implements OnInit {
|
||||
public utilityService: UtilityService, private toastr: ToastrService,
|
||||
private accountService: AccountService, public imageService: ImageService,
|
||||
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
|
||||
private confirmService: ConfirmService, private naturalSort: NaturalSortService,
|
||||
private confirmService: ConfirmService, private titleService: Title,
|
||||
private downloadService: DownloadService, private actionService: ActionService) {
|
||||
ratingConfig.max = 5;
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
@ -97,7 +96,7 @@ export class SeriesDetailComponent implements OnInit {
|
||||
const routeId = this.route.snapshot.paramMap.get('seriesId');
|
||||
const libraryId = this.route.snapshot.paramMap.get('libraryId');
|
||||
if (routeId === null || libraryId == null) {
|
||||
this.router.navigateByUrl('/home');
|
||||
this.router.navigateByUrl('/libraries');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -227,6 +226,8 @@ export class SeriesDetailComponent implements OnInit {
|
||||
this.seriesService.getSeries(seriesId).subscribe(series => {
|
||||
this.series = series;
|
||||
this.createHTML();
|
||||
|
||||
this.titleService.setTitle('Kavita - ' + this.series.name + ' Details');
|
||||
|
||||
|
||||
this.seriesService.getVolumes(this.series.id).subscribe(volumes => {
|
||||
@ -240,7 +241,6 @@ export class SeriesDetailComponent implements OnInit {
|
||||
this.specials = vol0.map(v => v.chapters || [])
|
||||
.flat()
|
||||
.filter(c => c.isSpecial || isNaN(parseInt(c.range, 10)))
|
||||
.sort((a, b) => this.naturalSort.compare(a.range, b.range, true))
|
||||
.map(c => {
|
||||
c.title = this.utilityService.cleanSpecialTitle(c.title);
|
||||
c.range = this.utilityService.cleanSpecialTitle(c.range);
|
||||
@ -255,6 +255,8 @@ export class SeriesDetailComponent implements OnInit {
|
||||
|
||||
this.isLoading = false;
|
||||
});
|
||||
}, err => {
|
||||
this.router.navigateByUrl('/libraries');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@ import { MangaFile } from 'src/app/_models/manga-file';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { NaturalSortService } from '../../_services/natural-sort.service';
|
||||
import { UtilityService } from '../../_services/utility.service';
|
||||
|
||||
|
||||
@ -27,7 +26,7 @@ export class CardDetailsModalComponent implements OnInit {
|
||||
formatKeys = Object.keys(MangaFormat);
|
||||
|
||||
constructor(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService,
|
||||
public imageService: ImageService, public naturalSort: NaturalSortService) { }
|
||||
public imageService: ImageService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.isObjectChapter(this.data);
|
||||
|
@ -2,6 +2,7 @@ import { Injectable, OnDestroy } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Soley repsonsible for performing a "natural" sort. This is the UI counterpart to the BE NaturalSortComparer.
|
||||
* Note: This does not work the same. Better to have the Backend perform the sort before sending to UI.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
@ -8,6 +8,7 @@ import { AccountService } from '../_services/account.service';
|
||||
import { Options } from '@angular-slider/ngx-slider';
|
||||
import { BookService } from '../book-reader/book.service';
|
||||
import { NavService } from '../_services/nav.service';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-preferences',
|
||||
@ -47,11 +48,12 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
fontFamilies: Array<string> = [];
|
||||
|
||||
constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService, private navService: NavService) {
|
||||
constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService, private navService: NavService, private titleService: Title) {
|
||||
this.fontFamilies = this.bookService.getFontFamilies();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.titleService.setTitle('Kavita - User Preferences');
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe((user: User) => {
|
||||
if (user) {
|
||||
this.user = user;
|
||||
|
Loading…
x
Reference in New Issue
Block a user