Foundational Rework (Round 2) (#2767)
This commit is contained in:
parent
304fd8bc79
commit
fc87dba0a7
29 changed files with 249 additions and 84 deletions
|
@ -105,6 +105,32 @@ public class ChapterListExtensionsTests
|
|||
Assert.Equal(chapterList[0], actualChapter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChapterByRange_On_FilenameChange_ShouldGetChapter()
|
||||
{
|
||||
var info = new ParserInfo()
|
||||
{
|
||||
Chapters = "1",
|
||||
Edition = "",
|
||||
Format = MangaFormat.Archive,
|
||||
FullFilePath = "/manga/detective comics #001.cbz",
|
||||
Filename = "detective comics #001.cbz",
|
||||
IsSpecial = false,
|
||||
Series = "detective comics",
|
||||
Title = "detective comics",
|
||||
Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume
|
||||
};
|
||||
|
||||
var chapterList = new List<Chapter>()
|
||||
{
|
||||
CreateChapter("1", "1", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), false),
|
||||
};
|
||||
|
||||
var actualChapter = chapterList.GetChapterByRange(info);
|
||||
|
||||
Assert.Equal(chapterList[0], actualChapter);
|
||||
}
|
||||
|
||||
#region GetFirstChapterWithFiles
|
||||
|
||||
[Fact]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using API.Entities.Enums;
|
||||
|
@ -33,7 +34,7 @@ public class ParserInfoListExtensions
|
|||
|
||||
[Theory]
|
||||
[InlineData(new[] {@"Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, true)]
|
||||
[InlineData(new[] {@"Cynthia The Mission - c000-006 (v06-07) [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, true)]
|
||||
[InlineData(new[] {@"Cynthia The Mission - c000-006 (v06-07) [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, false)]
|
||||
[InlineData(new[] {@"Cynthia The Mission v20 c12-20 [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, false)]
|
||||
public void HasInfoTest(string[] inputInfos, string[] inputChapters, bool expectedHasInfo)
|
||||
{
|
||||
|
@ -41,8 +42,8 @@ public class ParserInfoListExtensions
|
|||
foreach (var filename in inputInfos)
|
||||
{
|
||||
infos.Add(_defaultParser.Parse(
|
||||
filename,
|
||||
string.Empty));
|
||||
Path.Join("E:/Manga/Cynthia the Mission/", filename),
|
||||
"E:/Manga/"));
|
||||
}
|
||||
|
||||
var files = inputChapters.Select(s => new MangaFileBuilder(s, MangaFormat.Archive, 199).Build()).ToList();
|
||||
|
@ -52,4 +53,26 @@ public class ParserInfoListExtensions
|
|||
|
||||
Assert.Equal(expectedHasInfo, infos.HasInfo(chapter));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasInfoTest_SuccessWhenSpecial()
|
||||
{
|
||||
var infos = new[]
|
||||
{
|
||||
_defaultParser.Parse(
|
||||
"E:/Manga/Cynthia the Mission/Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip",
|
||||
"E:/Manga/")
|
||||
};
|
||||
|
||||
var files = new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip"}
|
||||
.Select(s => new MangaFileBuilder(s, MangaFormat.Archive, 199).Build())
|
||||
.ToList();
|
||||
var chapter = new ChapterBuilder("Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip")
|
||||
.WithRange("Cynthia The Mission The Special SP01 [Desudesu&Brolen]")
|
||||
.WithFiles(files)
|
||||
.WithIsSpecial(true)
|
||||
.Build();
|
||||
|
||||
Assert.True(infos.HasInfo(chapter));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -300,7 +300,7 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
Assert.Equal("2 - Ano Orokamono ni mo Kyakkou wo! - Volume 2", detail.Volumes.ElementAt(0).Name);
|
||||
|
||||
Assert.NotEmpty(detail.Specials);
|
||||
Assert.Equal("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", detail.Specials.ElementAt(0).Range);
|
||||
Assert.Equal("Ano Orokamono ni mo Kyakkou wo! - Volume 1", detail.Specials.ElementAt(0).Range);
|
||||
|
||||
// A book library where all books are Volumes, will show no "chapters" on the UI because it doesn't make sense
|
||||
Assert.Empty(detail.Chapters);
|
||||
|
|
|
@ -15,15 +15,22 @@ public class ChapterDto : IHasReadTimeEstimate
|
|||
/// <summary>
|
||||
/// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2". If special, will be special name.
|
||||
/// </summary>
|
||||
/// <remarks>This can be something like 19.HU or Alpha as some comics are like this</remarks>
|
||||
public string Range { get; init; } = default!;
|
||||
/// <summary>
|
||||
/// Smallest number of the Range.
|
||||
/// </summary>
|
||||
[Obsolete("Use MinNumber and MaxNumber instead")]
|
||||
public string Number { get; init; } = default!;
|
||||
/// <summary>
|
||||
/// This may be 0 under the circumstance that the Issue is "Alpha" or other non-standard numbers.
|
||||
/// </summary>
|
||||
public float MinNumber { get; init; }
|
||||
public float MaxNumber { get; init; }
|
||||
public float SortOrder { get; init; }
|
||||
/// <summary>
|
||||
/// The sorting order of the Chapter. Inherits from MinNumber, but can be overridden.
|
||||
/// </summary>
|
||||
public float SortOrder { get; set; }
|
||||
/// <summary>
|
||||
/// Total number of pages in all MangaFiles
|
||||
/// </summary>
|
||||
|
|
|
@ -60,7 +60,7 @@ public static class MigrateChapterFields
|
|||
"Running MigrateChapterFields migration - Updating all MangaFiles");
|
||||
foreach (var mangaFile in dataContext.MangaFile)
|
||||
{
|
||||
mangaFile.FileName = Path.GetFileNameWithoutExtension(mangaFile.FilePath);
|
||||
mangaFile.FileName = Parser.RemoveExtensionIfSupported(mangaFile.FilePath);
|
||||
}
|
||||
|
||||
var looseLeafChapters = await dataContext.Chapter.Where(c => c.Number == "0").ToListAsync();
|
||||
|
|
55
API/Data/ManualMigrations/MigrateChapterRange.cs
Normal file
55
API/Data/ManualMigrations/MigrateChapterRange.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.8.0 changed the range to that it doesn't have filename by default
|
||||
/// </summary>
|
||||
public static class MigrateChapterRange
|
||||
{
|
||||
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
|
||||
{
|
||||
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateChapterRange"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateChapterRange migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
var chapters = await dataContext.Chapter.ToListAsync();
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
if (Parser.MinNumberFromRange(chapter.Range) == 0.0f)
|
||||
{
|
||||
chapter.Range = chapter.GetNumberTitle();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Save changes after processing all series
|
||||
if (dataContext.ChangeTracker.HasChanges())
|
||||
{
|
||||
await dataContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateChapterRange",
|
||||
ProductVersion = BuildInfo.Version.ToString(),
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await dataContext.SaveChangesAsync();
|
||||
logger.LogCritical(
|
||||
"Running MigrateChapterRange migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
|
@ -498,6 +498,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Include(c => c.Files)
|
||||
.Where(c => EF.Functions.Like(c.TitleName, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(c.ISBN, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(c.Range, $"%{searchQuery}%")
|
||||
)
|
||||
.Where(c => c.Files.All(f => fileIds.Contains(f.Id)))
|
||||
.AsSplitQuery()
|
||||
|
|
|
@ -149,10 +149,13 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
|
|||
MinNumber = Parser.DefaultChapterNumber;
|
||||
MaxNumber = Parser.DefaultChapterNumber;
|
||||
}
|
||||
// NOTE: This doesn't work well for all because Pdf usually should use into.Title or even filename
|
||||
Title = (IsSpecial && info.Format == MangaFormat.Epub)
|
||||
? info.Title
|
||||
: Path.GetFileNameWithoutExtension(Range);
|
||||
: Parser.RemoveExtensionIfSupported(Range);
|
||||
|
||||
var specialTreatment = info.IsSpecialInfo();
|
||||
Range = specialTreatment ? info.Filename : info.Chapters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -165,13 +168,16 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
|
|||
{
|
||||
if (MinNumber.Is(Parser.DefaultChapterNumber) && IsSpecial)
|
||||
{
|
||||
return Title;
|
||||
return Parser.RemoveExtensionIfSupported(Title);
|
||||
}
|
||||
else
|
||||
|
||||
if (MinNumber.Is(0) && !float.TryParse(Range, out _))
|
||||
{
|
||||
return $"{MinNumber}";
|
||||
return $"{Range}";
|
||||
}
|
||||
|
||||
return $"{MinNumber}";
|
||||
|
||||
}
|
||||
return $"{MinNumber}-{MaxNumber}";
|
||||
}
|
||||
|
|
|
@ -29,10 +29,11 @@ public static class ChapterListExtensions
|
|||
/// <returns></returns>
|
||||
public static Chapter? GetChapterByRange(this IEnumerable<Chapter> chapters, ParserInfo info)
|
||||
{
|
||||
var normalizedPath = Parser.NormalizePath(info.FullFilePath);
|
||||
var specialTreatment = info.IsSpecialInfo();
|
||||
return specialTreatment
|
||||
? chapters.FirstOrDefault(c => c.Range == Path.GetFileNameWithoutExtension(info.Filename) || (c.Files.Select(f => f.FilePath).Contains(info.FullFilePath)))
|
||||
: chapters.FirstOrDefault(c => c.Range == info.Chapters);
|
||||
return specialTreatment
|
||||
? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath))
|
||||
: chapters.FirstOrDefault(c => c.Range == info.Chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
@ -27,7 +28,9 @@ public static class ParserInfoListExtensions
|
|||
/// <returns></returns>
|
||||
public static bool HasInfo(this IList<ParserInfo> infos, Chapter chapter)
|
||||
{
|
||||
return chapter.IsSpecial ? infos.Any(v => v.Filename == chapter.Range)
|
||||
: infos.Any(v => v.Chapters == chapter.Range);
|
||||
var chapterFiles = chapter.Files.Select(x => Parser.NormalizePath(x.FilePath)).ToList();
|
||||
var infoFiles = infos.Select(x => Parser.NormalizePath(x.FullFilePath)).ToList();
|
||||
return infoFiles.Intersect(chapterFiles).Any();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
@ -17,7 +18,7 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||
{
|
||||
_chapter = new Chapter()
|
||||
{
|
||||
Range = string.IsNullOrEmpty(range) ? number : range,
|
||||
Range = string.IsNullOrEmpty(range) ? number : Parser.RemoveExtensionIfSupported(range),
|
||||
Title = string.IsNullOrEmpty(range) ? number : range,
|
||||
Number = Parser.MinNumberFromRange(number).ToString(CultureInfo.InvariantCulture),
|
||||
MinNumber = Parser.MinNumberFromRange(number),
|
||||
|
@ -32,17 +33,14 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||
public static ChapterBuilder FromParserInfo(ParserInfo info)
|
||||
{
|
||||
var specialTreatment = info.IsSpecialInfo();
|
||||
var specialTitle = specialTreatment ? info.Filename : info.Chapters;
|
||||
var specialTitle = specialTreatment ? Parser.RemoveExtensionIfSupported(info.Filename) : info.Chapters;
|
||||
var builder = new ChapterBuilder(Parser.DefaultChapter);
|
||||
// TODO: Come back here and remove this side effect
|
||||
return builder.WithNumber(specialTreatment ? Parser.DefaultChapter : Parser.MinNumberFromRange(info.Chapters) + string.Empty)
|
||||
|
||||
return builder.WithNumber(Parser.RemoveExtensionIfSupported(info.Chapters))
|
||||
.WithRange(specialTreatment ? info.Filename : info.Chapters)
|
||||
.WithTitle((specialTreatment && info.Format == MangaFormat.Epub)
|
||||
? info.Title
|
||||
: specialTitle)
|
||||
// NEW
|
||||
//.WithTitle(string.IsNullOrEmpty(info.Filename) ? specialTitle : info.Filename)
|
||||
.WithTitle(info.Filename)
|
||||
.WithIsSpecial(specialTreatment);
|
||||
}
|
||||
|
||||
|
@ -53,7 +51,7 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||
}
|
||||
|
||||
|
||||
public ChapterBuilder WithNumber(string number)
|
||||
private ChapterBuilder WithNumber(string number)
|
||||
{
|
||||
_chapter.Number = number;
|
||||
_chapter.MinNumber = Parser.MinNumberFromRange(number);
|
||||
|
@ -79,11 +77,9 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||
return this;
|
||||
}
|
||||
|
||||
private ChapterBuilder WithRange(string range)
|
||||
public ChapterBuilder WithRange(string range)
|
||||
{
|
||||
_chapter.Range = range;
|
||||
// TODO: HACK: Overriding range
|
||||
_chapter.Range = _chapter.GetNumberTitle();
|
||||
_chapter.Range = Parser.RemoveExtensionIfSupported(range);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.IO;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
||||
namespace API.Helpers.Builders;
|
||||
|
||||
|
@ -19,7 +20,7 @@ public class MangaFileBuilder : IEntityBuilder<MangaFile>
|
|||
Pages = pages,
|
||||
LastModified = File.GetLastWriteTime(filePath),
|
||||
LastModifiedUtc = File.GetLastWriteTimeUtc(filePath),
|
||||
FileName = Path.GetFileNameWithoutExtension(filePath)
|
||||
FileName = Parser.RemoveExtensionIfSupported(filePath)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -82,6 +82,7 @@ public class MetadataService : IMetadataService
|
|||
chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath,
|
||||
ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, encodeFormat, coverImageSize);
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter));
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
|
|
@ -90,46 +90,6 @@ public class ReadingItemService : IReadingItemService
|
|||
|
||||
}
|
||||
|
||||
// This is first time ComicInfo is called
|
||||
info.ComicInfo = GetComicInfo(path);
|
||||
if (info.ComicInfo == null) return info;
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Volume))
|
||||
{
|
||||
info.Volumes = info.ComicInfo.Volume;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Series))
|
||||
{
|
||||
info.Series = info.ComicInfo.Series.Trim();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Number))
|
||||
{
|
||||
info.Chapters = info.ComicInfo.Number;
|
||||
}
|
||||
|
||||
// Patch is SeriesSort from ComicInfo
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.TitleSort))
|
||||
{
|
||||
info.SeriesSort = info.ComicInfo.TitleSort.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format))
|
||||
{
|
||||
info.IsSpecial = true;
|
||||
info.Chapters = Parser.DefaultChapter;
|
||||
info.Volumes = Parser.LooseLeafVolume;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.SeriesSort))
|
||||
{
|
||||
info.SeriesSort = info.ComicInfo.SeriesSort.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.LocalizedSeries))
|
||||
{
|
||||
info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim();
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
|
@ -218,6 +178,53 @@ public class ReadingItemService : IReadingItemService
|
|||
/// <returns></returns>
|
||||
private ParserInfo? Parse(string path, string rootPath, LibraryType type)
|
||||
{
|
||||
return Parser.IsEpub(path) ? _bookService.ParseInfo(path) : _defaultParser.Parse(path, rootPath, type);
|
||||
var info = Parser.IsEpub(path) ? _bookService.ParseInfo(path) : _defaultParser.Parse(path, rootPath, type);
|
||||
|
||||
if (info == null) return null;
|
||||
|
||||
info.ComicInfo = GetComicInfo(path);
|
||||
if (info.ComicInfo == null) return info;
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Volume))
|
||||
{
|
||||
info.Volumes = info.ComicInfo.Volume;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Series))
|
||||
{
|
||||
info.Series = info.ComicInfo.Series.Trim();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Number))
|
||||
{
|
||||
info.Chapters = info.ComicInfo.Number;
|
||||
if (info.IsSpecial && Parser.DefaultChapter != info.Chapters)
|
||||
{
|
||||
info.IsSpecial = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Patch is SeriesSort from ComicInfo
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.TitleSort))
|
||||
{
|
||||
info.SeriesSort = info.ComicInfo.TitleSort.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format))
|
||||
{
|
||||
info.IsSpecial = true;
|
||||
info.Chapters = Parser.DefaultChapter;
|
||||
info.Volumes = Parser.LooseLeafVolume;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.SeriesSort))
|
||||
{
|
||||
info.SeriesSort = info.ComicInfo.SeriesSort.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.LocalizedSeries))
|
||||
{
|
||||
info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim();
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -510,7 +510,7 @@ public class SeriesService : ISeriesService
|
|||
.SelectMany(v => v.Chapters
|
||||
.Select(c =>
|
||||
{
|
||||
if (v.IsLooseLeaf()) return c;
|
||||
if (v.IsLooseLeaf() || v.IsSpecial()) return c;
|
||||
c.VolumeTitle = v.Name;
|
||||
return c;
|
||||
})
|
||||
|
|
|
@ -386,6 +386,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
}
|
||||
if (RunningAnyTasksByMethod(ScanTasks, ScanQueue))
|
||||
{
|
||||
// BUG: This can end up triggering a ton of scan series calls
|
||||
_logger.LogInformation("A Scan is already running, rescheduling ScanSeries in 10 minutes");
|
||||
BackgroundJob.Schedule(() => ScanSeries(libraryId, seriesId, forceUpdate), TimeSpan.FromMinutes(10));
|
||||
return;
|
||||
|
|
|
@ -336,7 +336,7 @@ public class ParseScannedFiles
|
|||
MessageFactory.FileScanProgressEvent($"{files.Count} files in {folder}", library.Name, ProgressEventType.Updated));
|
||||
if (files.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("[ScannerService] {Folder} is empty or is no longer in this location", folder);
|
||||
_logger.LogInformation("[ScannerService] {Folder} is empty, no longer in this location, or has no file types that match Library File Types", folder);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -416,6 +416,12 @@ public class ParseScannedFiles
|
|||
}
|
||||
else
|
||||
{
|
||||
// TODO: I think I need to bump by 0.1f as if the prevIssue matches counter
|
||||
if (!string.IsNullOrEmpty(prevIssue) && prevIssue == counter + "")
|
||||
{
|
||||
// Bump by 0.1
|
||||
counter += 0.1f;
|
||||
}
|
||||
chapter.IssueOrder = counter;
|
||||
counter++;
|
||||
prevIssue = chapter.Chapters;
|
||||
|
|
|
@ -1130,4 +1130,15 @@ public static class Parser
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string RemoveExtensionIfSupported(string? filename)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename)) return filename;
|
||||
|
||||
if (Regex.IsMatch(filename, SupportedExtensions))
|
||||
{
|
||||
return Regex.Replace(filename, SupportedExtensions, string.Empty);
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -246,6 +246,7 @@ public class ProcessSeries : IProcessSeries
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[ScannerService] There was an exception updating series for {SeriesName}", series.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
|
@ -661,7 +662,7 @@ public class ProcessSeries : IProcessSeries
|
|||
{
|
||||
if (existingChapter.Files.Count == 0 || !parsedInfos.HasInfo(existingChapter))
|
||||
{
|
||||
_logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.GetNumberTitle(), volume.Name, parsedInfos[0].Series);
|
||||
_logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.Range, volume.Name, parsedInfos[0].Series);
|
||||
volume.Chapters.Remove(existingChapter);
|
||||
}
|
||||
else
|
||||
|
|
|
@ -257,6 +257,7 @@ public class Startup
|
|||
await MigrateChapterNumber.Migrate(dataContext, logger);
|
||||
await MigrateMixedSpecials.Migrate(dataContext, unitOfWork, logger);
|
||||
await MigrateChapterFields.Migrate(dataContext, unitOfWork, logger);
|
||||
await MigrateChapterRange.Migrate(dataContext, unitOfWork, logger);
|
||||
|
||||
// Update the version in the DB after all migrations are run
|
||||
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||
|
|
|
@ -50,4 +50,5 @@ export interface Chapter {
|
|||
webLinks: string;
|
||||
isbn: string;
|
||||
lastReadingProgress: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
|
|
@ -408,7 +408,7 @@
|
|||
<li class="d-flex my-4" *ngFor="let volume of seriesVolumes">
|
||||
<app-image class="me-3" style="width: 74px;" width="74px" [imageUrl]="imageService.getVolumeCoverImage(volume.id)"></app-image>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">{{t('volume-num')}} {{volume.name}}</h5>
|
||||
<h5 class="mt-0 mb-1">{{formatVolumeName(volume)}}</h5>
|
||||
<div>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
|
@ -432,7 +432,7 @@
|
|||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="volumeCollapsed[volume.name]">
|
||||
<ul class="list-group mt-2">
|
||||
<li *ngFor="let file of volume.volumeFiles.sort()" class="list-group-item">
|
||||
<li *ngFor="let file of volume.volumeFiles" class="list-group-item">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
|
|
|
@ -21,7 +21,7 @@ import { forkJoin, Observable, of } from 'rxjs';
|
|||
import { map } from 'rxjs/operators';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { TypeaheadSettings } from 'src/app/typeahead/_models/typeahead-settings';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import {Chapter, LooseLeafOrDefaultNumber, SpecialVolumeNumber} from 'src/app/_models/chapter';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import { Genre } from 'src/app/_models/metadata/genre';
|
||||
import { AgeRatingDto } from 'src/app/_models/metadata/age-rating-dto';
|
||||
|
@ -58,6 +58,7 @@ import {EditListComponent} from "../../../shared/edit-list/edit-list.component";
|
|||
import {AccountService} from "../../../_services/account.service";
|
||||
import {LibraryType} from "../../../_models/library/library";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {Volume} from "../../../_models/volume";
|
||||
|
||||
enum TabID {
|
||||
General = 0,
|
||||
|
@ -296,9 +297,10 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
this.volumeCollapsed[v.name] = true;
|
||||
});
|
||||
this.seriesVolumes.forEach(vol => {
|
||||
vol.volumeFiles = vol.chapters?.sort(this.utilityService.sortChapters).map((c: Chapter) => c.files.map((f: any) => {
|
||||
//.sort(this.utilityService.sortChapters) (no longer needed, all data is sorted on the backend)
|
||||
vol.volumeFiles = vol.chapters?.map((c: Chapter) => c.files.map((f: any) => {
|
||||
// TODO: Identify how to fix this hack
|
||||
f.chapter = c.number;
|
||||
f.chapter = c.range;
|
||||
return f;
|
||||
})).flat();
|
||||
});
|
||||
|
@ -316,6 +318,15 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
formatVolumeName(volume: Volume) {
|
||||
if (volume.minNumber === LooseLeafOrDefaultNumber) {
|
||||
return translate('edit-series-modal.loose-leaf-volume');
|
||||
} else if (volume.minNumber === SpecialVolumeNumber) {
|
||||
return translate('edit-series-modal.specials-volume');
|
||||
}
|
||||
return translate('edit-series-modal.volume-num') + ' ' + volume.name;
|
||||
}
|
||||
|
||||
|
||||
setupTypeaheads() {
|
||||
forkJoin([
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
<ul class="list-unstyled">
|
||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||
<!-- TODO: Localize title -->
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{utilityService.formatChapterName(libraryType, true, false)}} {{formatChapterNumber(chapter)}}">
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="imageService.getChapterCoverImage(chapter.id)"></app-image>
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
|
|
|
@ -186,7 +186,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
if (chapter.minNumber === LooseLeafOrDefaultNumber) {
|
||||
return '1';
|
||||
}
|
||||
return chapter.minNumber + '';
|
||||
return chapter.range + '';
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>, chapter: Chapter) {
|
||||
|
|
|
@ -198,9 +198,10 @@ export class CardItemComponent implements OnInit {
|
|||
this.format = (this.entity as Series).format;
|
||||
|
||||
if (this.utilityService.isChapter(this.entity)) {
|
||||
const chapterTitle = this.utilityService.asChapter(this.entity).titleName;
|
||||
const chapter = this.utilityService.asChapter(this.entity);
|
||||
const chapterTitle = chapter.titleName;
|
||||
if (chapterTitle === '' || chapterTitle === null || chapterTitle === undefined) {
|
||||
const volumeTitle = this.utilityService.asChapter(this.entity).volumeTitle
|
||||
const volumeTitle = chapter.volumeTitle
|
||||
if (volumeTitle === '' || volumeTitle === null || volumeTitle === undefined) {
|
||||
this.tooltipTitle = (this.title).trim();
|
||||
} else {
|
||||
|
|
|
@ -129,7 +129,8 @@
|
|||
<ng-container *ngIf="item.files.length > 0">
|
||||
<app-series-format [format]="item.files?.[0].format"></app-series-format>
|
||||
</ng-container>
|
||||
<span>{{item.titleName}}</span>
|
||||
<!-- TODO: this needs the series name before the chapter issue -->
|
||||
<span>{{item.titleName || item.range}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -1728,7 +1728,9 @@
|
|||
"highest-count-tooltip": "Highest Count found across all ComicInfo in the Series",
|
||||
"max-issue-tooltip": "Max Issue or Volume field from all ComicInfo in the series",
|
||||
"force-refresh": "Force Refresh",
|
||||
"force-refresh-tooltip": "Force refresh external metadata from Kavita+"
|
||||
"force-refresh-tooltip": "Force refresh external metadata from Kavita+",
|
||||
"loose-leaf-volume": "Loose Leaf Chapters",
|
||||
"specials-volume": "Specials"
|
||||
},
|
||||
|
||||
"day-breakdown": {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.14.3"
|
||||
"version": "0.7.14.5"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
@ -13953,6 +13953,7 @@
|
|||
},
|
||||
"minNumber": {
|
||||
"type": "number",
|
||||
"description": "This may be 0 under the circumstance that the Issue is \"Alpha\" or other non-standard numbers.",
|
||||
"format": "float"
|
||||
},
|
||||
"maxNumber": {
|
||||
|
@ -13961,6 +13962,7 @@
|
|||
},
|
||||
"sortOrder": {
|
||||
"type": "number",
|
||||
"description": "The sorting order of the Chapter. Inherits from MinNumber, but can be overridden.",
|
||||
"format": "float"
|
||||
},
|
||||
"pages": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue