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));
|
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);
|
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);
|
_scannerService = new ScannerService(unitOfWork, _logger, _archiveService, metadataService, _bookService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ namespace API.Controllers
|
|||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||||
|
private readonly NaturalSortComparer _naturalSortComparer = new NaturalSortComparer();
|
||||||
|
|
||||||
public ReaderController(IDirectoryService directoryService, ICacheService cacheService, IUnitOfWork unitOfWork)
|
public ReaderController(IDirectoryService directoryService, ICacheService cacheService, IUnitOfWork unitOfWork)
|
||||||
{
|
{
|
||||||
@ -295,8 +296,8 @@ namespace API.Controllers
|
|||||||
var currentChapter = await _unitOfWork.VolumeRepository.GetChapterAsync(currentChapterId);
|
var currentChapter = await _unitOfWork.VolumeRepository.GetChapterAsync(currentChapterId);
|
||||||
if (currentVolume.Number == 0)
|
if (currentVolume.Number == 0)
|
||||||
{
|
{
|
||||||
// Handle specials
|
// Handle specials by sorting on their Filename aka Range
|
||||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer), currentChapter.Number);
|
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Range, _naturalSortComparer), currentChapter.Number);
|
||||||
if (chapterId > 0) return Ok(chapterId);
|
if (chapterId > 0) return Ok(chapterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,7 +363,7 @@ namespace API.Controllers
|
|||||||
|
|
||||||
if (currentVolume.Number == 0)
|
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);
|
if (chapterId > 0) return Ok(chapterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.Comparators;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
@ -16,7 +18,7 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
private readonly DataContext _context;
|
private readonly DataContext _context;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
private readonly NaturalSortComparer _naturalSortComparer = new ();
|
||||||
public SeriesRepository(DataContext context, IMapper mapper)
|
public SeriesRepository(DataContext context, IMapper mapper)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
@ -37,7 +39,7 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
return await _context.SaveChangesAsync() > 0;
|
return await _context.SaveChangesAsync() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool SaveAll()
|
public bool SaveAll()
|
||||||
{
|
{
|
||||||
return _context.SaveChanges() > 0;
|
return _context.SaveChanges() > 0;
|
||||||
@ -60,12 +62,12 @@ namespace API.Data
|
|||||||
.Where(s => libraries.Contains(s.LibraryId) && s.Name == name)
|
.Where(s => libraries.Contains(s.LibraryId) && s.Name == name)
|
||||||
.CountAsync() > 1;
|
.CountAsync() > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Series GetSeriesByName(string name)
|
public Series GetSeriesByName(string name)
|
||||||
{
|
{
|
||||||
return _context.Series.SingleOrDefault(x => x.Name == name);
|
return _context.Series.SingleOrDefault(x => x.Name == name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId)
|
public async Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId)
|
||||||
{
|
{
|
||||||
return await _context.Series
|
return await _context.Series
|
||||||
@ -73,7 +75,7 @@ namespace API.Data
|
|||||||
.OrderBy(s => s.SortName)
|
.OrderBy(s => s.SortName)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams)
|
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams)
|
||||||
{
|
{
|
||||||
var query = _context.Series
|
var query = _context.Series
|
||||||
@ -84,12 +86,12 @@ namespace API.Data
|
|||||||
|
|
||||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<SearchResultDto>> SearchSeries(int[] libraryIds, string searchQuery)
|
public async Task<IEnumerable<SearchResultDto>> SearchSeries(int[] libraryIds, string searchQuery)
|
||||||
{
|
{
|
||||||
return await _context.Series
|
return await _context.Series
|
||||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
.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.OriginalName, $"%{searchQuery}%")
|
||||||
|| EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%"))
|
|| EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%"))
|
||||||
.Include(s => s.Library)
|
.Include(s => s.Library)
|
||||||
@ -108,12 +110,23 @@ namespace API.Data
|
|||||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
await AddVolumeModifiers(userId, volumes);
|
await AddVolumeModifiers(userId, volumes);
|
||||||
|
SortSpecialChapters(volumes);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return 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)
|
public async Task<IEnumerable<Volume>> GetVolumes(int seriesId)
|
||||||
{
|
{
|
||||||
@ -133,7 +146,7 @@ namespace API.Data
|
|||||||
|
|
||||||
var seriesList = new List<SeriesDto>() {series};
|
var seriesList = new List<SeriesDto>() {series};
|
||||||
await AddSeriesModifiers(userId, seriesList);
|
await AddSeriesModifiers(userId, seriesList);
|
||||||
|
|
||||||
return seriesList[0];
|
return seriesList[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +165,7 @@ namespace API.Data
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||||
.SingleAsync();
|
.SingleAsync();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId)
|
public async Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId)
|
||||||
@ -163,7 +176,7 @@ namespace API.Data
|
|||||||
.ThenInclude(c => c.Files)
|
.ThenInclude(c => c.Files)
|
||||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||||
.SingleAsync(vol => vol.Id == volumeId);
|
.SingleAsync(vol => vol.Id == volumeId);
|
||||||
|
|
||||||
var volumeList = new List<VolumeDto>() {volume};
|
var volumeList = new List<VolumeDto>() {volume};
|
||||||
await AddVolumeModifiers(userId, volumeList);
|
await AddVolumeModifiers(userId, volumeList);
|
||||||
|
|
||||||
@ -186,7 +199,7 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
var series = await _context.Series.Where(s => s.Id == seriesId).SingleOrDefaultAsync();
|
var series = await _context.Series.Where(s => s.Id == seriesId).SingleOrDefaultAsync();
|
||||||
_context.Series.Remove(series);
|
_context.Series.Remove(series);
|
||||||
|
|
||||||
return await _context.SaveChangesAsync() > 0;
|
return await _context.SaveChangesAsync() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +225,7 @@ namespace API.Data
|
|||||||
.Include(s => s.Volumes)
|
.Include(s => s.Volumes)
|
||||||
.ThenInclude(v => v.Chapters)
|
.ThenInclude(v => v.Chapters)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
IList<int> chapterIds = new List<int>();
|
IList<int> chapterIds = new List<int>();
|
||||||
foreach (var s in series)
|
foreach (var s in series)
|
||||||
{
|
{
|
||||||
@ -266,7 +279,7 @@ namespace API.Data
|
|||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddVolumeModifiers(int userId, List<VolumeDto> volumes)
|
private async Task AddVolumeModifiers(int userId, IReadOnlyCollection<VolumeDto> volumes)
|
||||||
{
|
{
|
||||||
var userProgress = await _context.AppUserProgresses
|
var userProgress = await _context.AppUserProgresses
|
||||||
.Where(p => p.AppUserId == userId && volumes.Select(s => s.Id).Contains(p.VolumeId))
|
.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);
|
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);
|
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);
|
return await PagedList<SeriesDto>.CreateAsync(allQuery, userParams.PageNumber, userParams.PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = _context.Series
|
var query = _context.Series
|
||||||
.Where(s => s.LibraryId == libraryId)
|
.Where(s => s.LibraryId == libraryId)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@ -356,7 +369,7 @@ namespace API.Data
|
|||||||
{
|
{
|
||||||
series = series.Where(s => s.AppUserId == userId
|
series = series.Where(s => s.AppUserId == userId
|
||||||
&& s.PagesRead > 0
|
&& s.PagesRead > 0
|
||||||
&& s.PagesRead < s.Series.Pages
|
&& s.PagesRead < s.Series.Pages
|
||||||
&& s.Series.LibraryId == libraryId);
|
&& s.Series.LibraryId == libraryId);
|
||||||
}
|
}
|
||||||
var retSeries = await series
|
var retSeries = await series
|
||||||
@ -365,7 +378,7 @@ namespace API.Data
|
|||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
return retSeries.DistinctBy(s => s.Name).Take(limit);
|
return retSeries.DistinctBy(s => s.Name).Take(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,7 +399,7 @@ namespace API.Data
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataDto;
|
return metadataDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,7 +411,7 @@ namespace API.Data
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Select(library => library.Id)
|
.Select(library => library.Id)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var query = _context.CollectionTag
|
var query = _context.CollectionTag
|
||||||
.Where(s => s.Id == collectionId)
|
.Where(s => s.Id == collectionId)
|
||||||
.Include(c => c.SeriesMetadatas)
|
.Include(c => c.SeriesMetadatas)
|
||||||
@ -423,4 +436,4 @@ namespace API.Data
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
using API.Services;
|
|
||||||
using API.Services.Tasks.Scanner;
|
using API.Services.Tasks.Scanner;
|
||||||
|
|
||||||
namespace API.Extensions
|
namespace API.Extensions
|
||||||
|
@ -498,6 +498,12 @@ namespace API.Parser
|
|||||||
ret.Series = CleanTitle(fileName);
|
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;
|
return ret.Series == string.Empty ? null : ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,8 +60,15 @@ namespace API.Services
|
|||||||
if (files.Count > 0 && files[0].Format == MangaFormat.Image)
|
if (files.Count > 0 && files[0].Format == MangaFormat.Image)
|
||||||
{
|
{
|
||||||
DirectoryService.ExistOrCreate(extractPath);
|
DirectoryService.ExistOrCreate(extractPath);
|
||||||
var pattern = (files.Count == 1) ? (@"\" + Path.GetExtension(files[0].FilePath)) : Parser.Parser.ImageFileExtensions;
|
if (files.Count == 1)
|
||||||
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, pattern);
|
{
|
||||||
|
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(files[0].FilePath), extractPath, Parser.Parser.ImageFileExtensions);
|
||||||
|
}
|
||||||
|
|
||||||
extractDi.Flatten();
|
extractDi.Flatten();
|
||||||
return chapter;
|
return chapter;
|
||||||
}
|
}
|
||||||
|
@ -20,19 +20,17 @@ namespace API.Services
|
|||||||
private readonly ILogger<MetadataService> _logger;
|
private readonly ILogger<MetadataService> _logger;
|
||||||
private readonly IArchiveService _archiveService;
|
private readonly IArchiveService _archiveService;
|
||||||
private readonly IBookService _bookService;
|
private readonly IBookService _bookService;
|
||||||
private readonly IDirectoryService _directoryService;
|
|
||||||
private readonly IImageService _imageService;
|
private readonly IImageService _imageService;
|
||||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||||
public static readonly int ThumbnailWidth = 320; // 153w x 230h
|
public static readonly int ThumbnailWidth = 320; // 153w x 230h
|
||||||
|
|
||||||
public MetadataService(IUnitOfWork unitOfWork, ILogger<MetadataService> logger,
|
public MetadataService(IUnitOfWork unitOfWork, ILogger<MetadataService> logger,
|
||||||
IArchiveService archiveService, IBookService bookService, IDirectoryService directoryService, IImageService imageService)
|
IArchiveService archiveService, IBookService bookService, IImageService imageService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_archiveService = archiveService;
|
_archiveService = archiveService;
|
||||||
_bookService = bookService;
|
_bookService = bookService;
|
||||||
_directoryService = directoryService;
|
|
||||||
_imageService = imageService;
|
_imageService = imageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,25 +69,24 @@ namespace API.Services
|
|||||||
|
|
||||||
public void UpdateMetadata(Volume volume, bool forceUpdate)
|
public void UpdateMetadata(Volume volume, bool forceUpdate)
|
||||||
{
|
{
|
||||||
if (volume != null && ShouldFindCoverImage(volume.CoverImage, forceUpdate))
|
if (volume == null || !ShouldFindCoverImage(volume.CoverImage, forceUpdate)) return;
|
||||||
{
|
|
||||||
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
|
volume.Chapters ??= new List<Chapter>();
|
||||||
if (firstChapter == null || ShouldFindCoverImage(firstChapter.CoverImage))
|
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();
|
volume.CoverImage = GetCoverImage(firstFile);
|
||||||
if (firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified))
|
|
||||||
{
|
|
||||||
volume.CoverImage = GetCoverImage(firstFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
volume.CoverImage = firstChapter.CoverImage;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
volume.CoverImage = firstChapter.CoverImage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateMetadata(Series series, bool forceUpdate)
|
public void UpdateMetadata(Series series, bool forceUpdate)
|
||||||
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
@ -38,6 +39,20 @@ namespace API.Services.Tasks.Scanner
|
|||||||
_scannedSeries = new ConcurrentDictionary<ParsedSeries, List<ParserInfo>>();
|
_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>
|
/// <summary>
|
||||||
/// Processes files found during a library scan.
|
/// Processes files found during a library scan.
|
||||||
/// Populates a collection of <see cref="ParserInfo"/> for DB updates later.
|
/// Populates a collection of <see cref="ParserInfo"/> for DB updates later.
|
||||||
@ -144,7 +159,7 @@ namespace API.Services.Tasks.Scanner
|
|||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
totalFiles = 0;
|
totalFiles = 0;
|
||||||
var searchPattern = GetLibrarySearchPattern(libraryType);
|
var searchPattern = GetLibrarySearchPattern();
|
||||||
foreach (var folderPath in folders)
|
foreach (var folderPath in folders)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -174,12 +189,7 @@ namespace API.Services.Tasks.Scanner
|
|||||||
return SeriesWithInfos();
|
return SeriesWithInfos();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static string GetLibrarySearchPattern()
|
||||||
/// 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)
|
|
||||||
{
|
{
|
||||||
return Parser.Parser.SupportedExtensions;
|
return Parser.Parser.SupportedExtensions;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -27,7 +26,7 @@ namespace API.Services.Tasks
|
|||||||
private readonly IArchiveService _archiveService;
|
private readonly IArchiveService _archiveService;
|
||||||
private readonly IMetadataService _metadataService;
|
private readonly IMetadataService _metadataService;
|
||||||
private readonly IBookService _bookService;
|
private readonly IBookService _bookService;
|
||||||
private readonly NaturalSortComparer _naturalSort;
|
private readonly NaturalSortComparer _naturalSort = new ();
|
||||||
|
|
||||||
public ScannerService(IUnitOfWork unitOfWork, ILogger<ScannerService> logger, IArchiveService archiveService,
|
public ScannerService(IUnitOfWork unitOfWork, ILogger<ScannerService> logger, IArchiveService archiveService,
|
||||||
IMetadataService metadataService, IBookService bookService)
|
IMetadataService metadataService, IBookService bookService)
|
||||||
@ -37,7 +36,6 @@ namespace API.Services.Tasks
|
|||||||
_archiveService = archiveService;
|
_archiveService = archiveService;
|
||||||
_metadataService = metadataService;
|
_metadataService = metadataService;
|
||||||
_bookService = bookService;
|
_bookService = bookService;
|
||||||
_naturalSort = new NaturalSortComparer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DisableConcurrentExecution(timeoutInSeconds: 360)]
|
[DisableConcurrentExecution(timeoutInSeconds: 360)]
|
||||||
@ -200,10 +198,16 @@ namespace API.Services.Tasks
|
|||||||
_logger.LogInformation("Removed {RemoveMissingSeries} series that are no longer on disk:", removeCount);
|
_logger.LogInformation("Removed {RemoveMissingSeries} series that are no longer on disk:", removeCount);
|
||||||
foreach (var s in missingSeries)
|
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
|
// Add new series that have parsedInfos
|
||||||
foreach (var (key, infos) in parsedSeries)
|
foreach (var (key, infos) in parsedSeries)
|
||||||
@ -218,11 +222,11 @@ namespace API.Services.Tasks
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
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();
|
var duplicateSeries = library.Series.Where(s => s.NormalizedName == key.NormalizedName || Parser.Parser.Normalize(s.OriginalName) == key.NormalizedName).ToList();
|
||||||
foreach (var series in duplicateSeries)
|
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;
|
continue;
|
||||||
@ -247,7 +251,7 @@ namespace API.Services.Tasks
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Processing series {SeriesName}", series.OriginalName);
|
_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);
|
series.Pages = series.Volumes.Sum(v => v.Pages);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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)
|
public IEnumerable<Series> FindSeriesNotOnDisk(ICollection<Series> existingSeries, Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries)
|
||||||
{
|
{
|
||||||
var foundSeries = parsedSeries.Select(s => s.Key.Name).ToList();
|
// It is safe to check only first since Parser ensures that a Series only has one type
|
||||||
return existingSeries.Where(es => !es.NameInList(foundSeries));
|
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>
|
/// <summary>
|
||||||
@ -293,7 +287,7 @@ namespace API.Services.Tasks
|
|||||||
|
|
||||||
existingSeries = existingSeries.Where(
|
existingSeries = existingSeries.Where(
|
||||||
s => !missingList.Exists(
|
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;
|
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.');
|
this.toastr.error('You are not authorized to view this page.');
|
||||||
localStorage.setItem(this.urlKey, window.location.pathname);
|
localStorage.setItem(this.urlKey, window.location.pathname);
|
||||||
this.router.navigateByUrl('/home');
|
this.router.navigateByUrl('/libraries');
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { ServerService } from 'src/app/_services/server.service';
|
import { ServerService } from 'src/app/_services/server.service';
|
||||||
import { saveAs } from 'file-saver';
|
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;
|
counter = this.tabs.length + 1;
|
||||||
active = this.tabs[0];
|
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 => {
|
this.route.fragment.subscribe(frag => {
|
||||||
const tab = this.tabs.filter(item => item.fragment === frag);
|
const tab = this.tabs.filter(item => item.fragment === frag);
|
||||||
if (tab.length > 0) {
|
if (tab.length > 0) {
|
||||||
@ -34,7 +35,9 @@ export class DashboardComponent implements OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {}
|
ngOnInit() {
|
||||||
|
this.titleService.setTitle('Kavita - Admin Dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
restartServer() {
|
restartServer() {
|
||||||
this.serverService.restart().subscribe(() => {
|
this.serverService.restart().subscribe(() => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
@ -28,7 +29,9 @@ export class AllCollectionsComponent implements OnInit {
|
|||||||
seriesPagination!: Pagination;
|
seriesPagination!: Pagination;
|
||||||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
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;
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
|
|
||||||
const routeId = this.route.snapshot.paramMap.get('id');
|
const routeId = this.route.snapshot.paramMap.get('id');
|
||||||
@ -43,6 +46,7 @@ export class AllCollectionsComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.collectionTagName = tags.filter(item => item.id === this.collectionTagId)[0].title;
|
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 { take } from 'rxjs/operators';
|
||||||
import { MemberService } from '../_services/member.service';
|
import { MemberService } from '../_services/member.service';
|
||||||
import { AccountService } from '../_services/account.service';
|
import { AccountService } from '../_services/account.service';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
@ -19,7 +20,7 @@ export class HomeComponent implements OnInit {
|
|||||||
password: new FormControl('', [Validators.required])
|
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 {
|
ngOnInit(): void {
|
||||||
@ -31,6 +32,7 @@ export class HomeComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.titleService.setTitle('Kavita');
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
if (user) {
|
if (user) {
|
||||||
this.router.navigateByUrl('/library');
|
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) {
|
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService, private actionService: ActionService) {
|
||||||
const routeId = this.route.snapshot.paramMap.get('id');
|
const routeId = this.route.snapshot.paramMap.get('id');
|
||||||
if (routeId === null) {
|
if (routeId === null) {
|
||||||
this.router.navigateByUrl('/home');
|
this.router.navigateByUrl('/libraries');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
@ -34,9 +35,13 @@ export class LibraryComponent implements OnInit {
|
|||||||
|
|
||||||
seriesTrackBy = (index: number, item: any) => `${item.name}_${item.pagesRead}`;
|
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 {
|
ngOnInit(): void {
|
||||||
|
this.titleService.setTitle('Kavita - Dashboard');
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
@ -245,7 +245,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
const chapterId = this.route.snapshot.paramMap.get('chapterId');
|
const chapterId = this.route.snapshot.paramMap.get('chapterId');
|
||||||
|
|
||||||
if (libraryId === null || seriesId === null || chapterId === null) {
|
if (libraryId === null || seriesId === null || chapterId === null) {
|
||||||
this.router.navigateByUrl('/home');
|
this.router.navigateByUrl('/libraries');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
|||||||
logout() {
|
logout() {
|
||||||
this.accountService.logout();
|
this.accountService.logout();
|
||||||
this.navService.hideNavBar();
|
this.navService.hideNavBar();
|
||||||
this.router.navigateByUrl('/home');
|
this.router.navigateByUrl('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
moveFocus() {
|
moveFocus() {
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { forkJoin } from 'rxjs';
|
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config';
|
import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config';
|
||||||
import { ConfirmService } from '../shared/confirm.service';
|
import { ConfirmService } from '../shared/confirm.service';
|
||||||
import { CardDetailsModalComponent } from '../shared/_modals/card-details-modal/card-details-modal.component';
|
import { CardDetailsModalComponent } from '../shared/_modals/card-details-modal/card-details-modal.component';
|
||||||
import { DownloadService } from '../shared/_services/download.service';
|
import { DownloadService } from '../shared/_services/download.service';
|
||||||
import { NaturalSortService } from '../shared/_services/natural-sort.service';
|
|
||||||
import { UtilityService } from '../shared/_services/utility.service';
|
import { UtilityService } from '../shared/_services/utility.service';
|
||||||
import { EditSeriesModalComponent } from '../_modals/edit-series-modal/edit-series-modal.component';
|
import { EditSeriesModalComponent } from '../_modals/edit-series-modal/edit-series-modal.component';
|
||||||
import { ReviewSeriesModalComponent } from '../_modals/review-series-modal/review-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,
|
public utilityService: UtilityService, private toastr: ToastrService,
|
||||||
private accountService: AccountService, public imageService: ImageService,
|
private accountService: AccountService, public imageService: ImageService,
|
||||||
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
|
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
|
||||||
private confirmService: ConfirmService, private naturalSort: NaturalSortService,
|
private confirmService: ConfirmService, private titleService: Title,
|
||||||
private downloadService: DownloadService, private actionService: ActionService) {
|
private downloadService: DownloadService, private actionService: ActionService) {
|
||||||
ratingConfig.max = 5;
|
ratingConfig.max = 5;
|
||||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
@ -97,7 +96,7 @@ export class SeriesDetailComponent implements OnInit {
|
|||||||
const routeId = this.route.snapshot.paramMap.get('seriesId');
|
const routeId = this.route.snapshot.paramMap.get('seriesId');
|
||||||
const libraryId = this.route.snapshot.paramMap.get('libraryId');
|
const libraryId = this.route.snapshot.paramMap.get('libraryId');
|
||||||
if (routeId === null || libraryId == null) {
|
if (routeId === null || libraryId == null) {
|
||||||
this.router.navigateByUrl('/home');
|
this.router.navigateByUrl('/libraries');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,6 +226,8 @@ export class SeriesDetailComponent implements OnInit {
|
|||||||
this.seriesService.getSeries(seriesId).subscribe(series => {
|
this.seriesService.getSeries(seriesId).subscribe(series => {
|
||||||
this.series = series;
|
this.series = series;
|
||||||
this.createHTML();
|
this.createHTML();
|
||||||
|
|
||||||
|
this.titleService.setTitle('Kavita - ' + this.series.name + ' Details');
|
||||||
|
|
||||||
|
|
||||||
this.seriesService.getVolumes(this.series.id).subscribe(volumes => {
|
this.seriesService.getVolumes(this.series.id).subscribe(volumes => {
|
||||||
@ -240,7 +241,6 @@ export class SeriesDetailComponent implements OnInit {
|
|||||||
this.specials = vol0.map(v => v.chapters || [])
|
this.specials = vol0.map(v => v.chapters || [])
|
||||||
.flat()
|
.flat()
|
||||||
.filter(c => c.isSpecial || isNaN(parseInt(c.range, 10)))
|
.filter(c => c.isSpecial || isNaN(parseInt(c.range, 10)))
|
||||||
.sort((a, b) => this.naturalSort.compare(a.range, b.range, true))
|
|
||||||
.map(c => {
|
.map(c => {
|
||||||
c.title = this.utilityService.cleanSpecialTitle(c.title);
|
c.title = this.utilityService.cleanSpecialTitle(c.title);
|
||||||
c.range = this.utilityService.cleanSpecialTitle(c.range);
|
c.range = this.utilityService.cleanSpecialTitle(c.range);
|
||||||
@ -255,6 +255,8 @@ export class SeriesDetailComponent implements OnInit {
|
|||||||
|
|
||||||
this.isLoading = false;
|
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 { MangaFormat } from 'src/app/_models/manga-format';
|
||||||
import { Volume } from 'src/app/_models/volume';
|
import { Volume } from 'src/app/_models/volume';
|
||||||
import { ImageService } from 'src/app/_services/image.service';
|
import { ImageService } from 'src/app/_services/image.service';
|
||||||
import { NaturalSortService } from '../../_services/natural-sort.service';
|
|
||||||
import { UtilityService } from '../../_services/utility.service';
|
import { UtilityService } from '../../_services/utility.service';
|
||||||
|
|
||||||
|
|
||||||
@ -27,7 +26,7 @@ export class CardDetailsModalComponent implements OnInit {
|
|||||||
formatKeys = Object.keys(MangaFormat);
|
formatKeys = Object.keys(MangaFormat);
|
||||||
|
|
||||||
constructor(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService,
|
constructor(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService,
|
||||||
public imageService: ImageService, public naturalSort: NaturalSortService) { }
|
public imageService: ImageService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.isChapter = this.isObjectChapter(this.data);
|
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.
|
* 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({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
|
@ -8,6 +8,7 @@ import { AccountService } from '../_services/account.service';
|
|||||||
import { Options } from '@angular-slider/ngx-slider';
|
import { Options } from '@angular-slider/ngx-slider';
|
||||||
import { BookService } from '../book-reader/book.service';
|
import { BookService } from '../book-reader/book.service';
|
||||||
import { NavService } from '../_services/nav.service';
|
import { NavService } from '../_services/nav.service';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-preferences',
|
selector: 'app-user-preferences',
|
||||||
@ -47,11 +48,12 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
fontFamilies: Array<string> = [];
|
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();
|
this.fontFamilies = this.bookService.getFontFamilies();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.titleService.setTitle('Kavita - User Preferences');
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe((user: User) => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe((user: User) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user