|
|
|
|
@@ -190,6 +190,48 @@ public class NovelUpdateService
|
|
|
|
|
return existingChapters.Concat(newChapters).ToList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static List<Volume> SynchronizeVolumes(
|
|
|
|
|
List<VolumeMetadata> metadataVolumes,
|
|
|
|
|
Language rawLanguage,
|
|
|
|
|
List<Volume>? existingVolumes)
|
|
|
|
|
{
|
|
|
|
|
existingVolumes ??= new List<Volume>();
|
|
|
|
|
var result = new List<Volume>();
|
|
|
|
|
|
|
|
|
|
foreach (var metaVolume in metadataVolumes)
|
|
|
|
|
{
|
|
|
|
|
// Match volumes by Order (unique per novel)
|
|
|
|
|
var existingVolume = existingVolumes.FirstOrDefault(v => v.Order == metaVolume.Order);
|
|
|
|
|
|
|
|
|
|
if (existingVolume != null)
|
|
|
|
|
{
|
|
|
|
|
// Volume exists - sync its chapters
|
|
|
|
|
existingVolume.Chapters = SynchronizeChapters(
|
|
|
|
|
metaVolume.Chapters,
|
|
|
|
|
rawLanguage,
|
|
|
|
|
existingVolume.Chapters);
|
|
|
|
|
result.Add(existingVolume);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// New volume - create it with synced chapters
|
|
|
|
|
var newVolume = new Volume
|
|
|
|
|
{
|
|
|
|
|
Order = metaVolume.Order,
|
|
|
|
|
Name = LocalizationKey.CreateFromText(metaVolume.Name, rawLanguage),
|
|
|
|
|
Chapters = SynchronizeChapters(metaVolume.Chapters, rawLanguage, null)
|
|
|
|
|
};
|
|
|
|
|
result.Add(newVolume);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep existing volumes not in metadata (user-created volumes)
|
|
|
|
|
var metaOrders = metadataVolumes.Select(v => v.Order).ToHashSet();
|
|
|
|
|
result.AddRange(existingVolumes.Where(v => !metaOrders.Contains(v.Order)));
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static (Image? image, bool shouldPublishEvent) HandleCoverImage(
|
|
|
|
|
ImageData? newCoverData,
|
|
|
|
|
Image? existingCoverImage)
|
|
|
|
|
@@ -232,7 +274,7 @@ public class NovelUpdateService
|
|
|
|
|
metadata.SystemTags,
|
|
|
|
|
metadata.RawLanguage);
|
|
|
|
|
|
|
|
|
|
var chapters = SynchronizeChapters(metadata.Chapters, metadata.RawLanguage, null);
|
|
|
|
|
var volumes = SynchronizeVolumes(metadata.Volumes, metadata.RawLanguage, null);
|
|
|
|
|
|
|
|
|
|
var novel = new Novel
|
|
|
|
|
{
|
|
|
|
|
@@ -243,7 +285,7 @@ public class NovelUpdateService
|
|
|
|
|
CoverImage = metadata.CoverImage != null
|
|
|
|
|
? new Image { OriginalPath = metadata.CoverImage.Url }
|
|
|
|
|
: null,
|
|
|
|
|
Chapters = chapters,
|
|
|
|
|
Volumes = volumes,
|
|
|
|
|
Description = LocalizationKey.CreateFromText(metadata.Description, metadata.RawLanguage),
|
|
|
|
|
Name = LocalizationKey.CreateFromText(metadata.Name, metadata.RawLanguage),
|
|
|
|
|
RawStatus = metadata.RawStatus,
|
|
|
|
|
@@ -289,7 +331,9 @@ public class NovelUpdateService
|
|
|
|
|
.Include(n => n.Description)
|
|
|
|
|
.ThenInclude(lk => lk.Texts)
|
|
|
|
|
.Include(n => n.Tags)
|
|
|
|
|
.Include(n => n.Chapters).ThenInclude(chapter => chapter.Body)
|
|
|
|
|
.Include(n => n.Volumes)
|
|
|
|
|
.ThenInclude(volume => volume.Chapters)
|
|
|
|
|
.ThenInclude(chapter => chapter.Body)
|
|
|
|
|
.ThenInclude(localizationKey => localizationKey.Texts)
|
|
|
|
|
.Include(n => n.CoverImage)
|
|
|
|
|
.FirstOrDefaultAsync(n =>
|
|
|
|
|
@@ -326,11 +370,11 @@ public class NovelUpdateService
|
|
|
|
|
metadata.SystemTags,
|
|
|
|
|
metadata.RawLanguage);
|
|
|
|
|
|
|
|
|
|
// Synchronize chapters (add only)
|
|
|
|
|
novel.Chapters = SynchronizeChapters(
|
|
|
|
|
metadata.Chapters,
|
|
|
|
|
// Synchronize volumes (and their chapters)
|
|
|
|
|
novel.Volumes = SynchronizeVolumes(
|
|
|
|
|
metadata.Volumes,
|
|
|
|
|
metadata.RawLanguage,
|
|
|
|
|
existingNovel.Chapters);
|
|
|
|
|
existingNovel.Volumes);
|
|
|
|
|
|
|
|
|
|
// Handle cover image
|
|
|
|
|
(novel.CoverImage, shouldPublishCoverEvent) = HandleCoverImage(
|
|
|
|
|
@@ -352,31 +396,40 @@ public class NovelUpdateService
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Publish chapter pull events for chapters without body content
|
|
|
|
|
var chaptersNeedingPull = novel.Chapters
|
|
|
|
|
.Where(c => c.Body?.Texts == null || !c.Body.Texts.Any())
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
foreach (var chapter in chaptersNeedingPull)
|
|
|
|
|
foreach (var volume in novel.Volumes)
|
|
|
|
|
{
|
|
|
|
|
await _eventBus.Publish(new ChapterPullRequestedEvent
|
|
|
|
|
var chaptersNeedingPull = volume.Chapters
|
|
|
|
|
.Where(c => c.Body?.Texts == null || !c.Body.Texts.Any())
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
foreach (var chapter in chaptersNeedingPull)
|
|
|
|
|
{
|
|
|
|
|
NovelId = novel.Id,
|
|
|
|
|
ChapterNumber = chapter.Order
|
|
|
|
|
});
|
|
|
|
|
await _eventBus.Publish(new ChapterPullRequestedEvent
|
|
|
|
|
{
|
|
|
|
|
NovelId = novel.Id,
|
|
|
|
|
VolumeId = volume.Id,
|
|
|
|
|
ChapterOrder = chapter.Order
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return novel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<Chapter> PullChapterContents(uint novelId, uint chapterNumber)
|
|
|
|
|
public async Task<Chapter> PullChapterContents(uint novelId, uint volumeId, uint chapterOrder)
|
|
|
|
|
{
|
|
|
|
|
var novel = await _dbContext.Novels.Where(novel => novel.Id == novelId)
|
|
|
|
|
.Include(novel => novel.Chapters)
|
|
|
|
|
.Include(novel => novel.Volumes)
|
|
|
|
|
.ThenInclude(volume => volume.Chapters)
|
|
|
|
|
.ThenInclude(chapter => chapter.Body)
|
|
|
|
|
.ThenInclude(body => body.Texts)
|
|
|
|
|
.Include(novel => novel.Source).Include(novel => novel.Chapters).ThenInclude(chapter => chapter.Images)
|
|
|
|
|
.Include(novel => novel.Source)
|
|
|
|
|
.Include(novel => novel.Volumes)
|
|
|
|
|
.ThenInclude(volume => volume.Chapters)
|
|
|
|
|
.ThenInclude(chapter => chapter.Images)
|
|
|
|
|
.FirstOrDefaultAsync();
|
|
|
|
|
var chapter = novel.Chapters.Where(chapter => chapter.Order == chapterNumber).FirstOrDefault();
|
|
|
|
|
var volume = novel.Volumes.FirstOrDefault(v => v.Id == volumeId);
|
|
|
|
|
var chapter = volume.Chapters.FirstOrDefault(c => c.Order == chapterOrder);
|
|
|
|
|
var adapter = _sourceAdapters.FirstOrDefault(adapter => adapter.SourceDescriptor.Key == novel.Source.Key);
|
|
|
|
|
var rawChapter = await adapter.GetRawChapter(chapter.Url);
|
|
|
|
|
|
|
|
|
|
@@ -478,12 +531,13 @@ public class NovelUpdateService
|
|
|
|
|
return importNovelRequestEvent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<ChapterPullRequestedEvent> QueueChapterPull(uint novelId, uint chapterNumber)
|
|
|
|
|
public async Task<ChapterPullRequestedEvent> QueueChapterPull(uint novelId, uint volumeId, uint chapterOrder)
|
|
|
|
|
{
|
|
|
|
|
var chapterPullEvent = new ChapterPullRequestedEvent()
|
|
|
|
|
{
|
|
|
|
|
NovelId = novelId,
|
|
|
|
|
ChapterNumber = chapterNumber
|
|
|
|
|
VolumeId = volumeId,
|
|
|
|
|
ChapterOrder = chapterOrder
|
|
|
|
|
};
|
|
|
|
|
await _eventBus.Publish(chapterPullEvent);
|
|
|
|
|
return chapterPullEvent;
|
|
|
|
|
@@ -495,9 +549,10 @@ public class NovelUpdateService
|
|
|
|
|
.Include(n => n.CoverImage)
|
|
|
|
|
.Include(n => n.Name).ThenInclude(k => k.Texts)
|
|
|
|
|
.Include(n => n.Description).ThenInclude(k => k.Texts)
|
|
|
|
|
.Include(n => n.Chapters).ThenInclude(c => c.Images)
|
|
|
|
|
.Include(n => n.Chapters).ThenInclude(c => c.Name).ThenInclude(k => k.Texts)
|
|
|
|
|
.Include(n => n.Chapters).ThenInclude(c => c.Body).ThenInclude(k => k.Texts)
|
|
|
|
|
.Include(n => n.Volumes).ThenInclude(v => v.Name).ThenInclude(k => k.Texts)
|
|
|
|
|
.Include(n => n.Volumes).ThenInclude(v => v.Chapters).ThenInclude(c => c.Images)
|
|
|
|
|
.Include(n => n.Volumes).ThenInclude(v => v.Chapters).ThenInclude(c => c.Name).ThenInclude(k => k.Texts)
|
|
|
|
|
.Include(n => n.Volumes).ThenInclude(v => v.Chapters).ThenInclude(c => c.Body).ThenInclude(k => k.Texts)
|
|
|
|
|
.FirstOrDefaultAsync(n => n.Id == novelId);
|
|
|
|
|
|
|
|
|
|
if (novel == null)
|
|
|
|
|
@@ -505,8 +560,12 @@ public class NovelUpdateService
|
|
|
|
|
|
|
|
|
|
// Collect all LocalizationKey IDs for cleanup
|
|
|
|
|
var locKeyIds = new List<Guid> { novel.Name.Id, novel.Description.Id };
|
|
|
|
|
locKeyIds.AddRange(novel.Chapters.Select(c => c.Name.Id));
|
|
|
|
|
locKeyIds.AddRange(novel.Chapters.Select(c => c.Body.Id));
|
|
|
|
|
foreach (var volume in novel.Volumes)
|
|
|
|
|
{
|
|
|
|
|
locKeyIds.Add(volume.Name.Id);
|
|
|
|
|
locKeyIds.AddRange(volume.Chapters.Select(c => c.Name.Id));
|
|
|
|
|
locKeyIds.AddRange(volume.Chapters.Select(c => c.Body.Id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1. Remove LocalizationRequests referencing these keys
|
|
|
|
|
var locRequests = await _dbContext.LocalizationRequests
|
|
|
|
|
@@ -517,19 +576,26 @@ public class NovelUpdateService
|
|
|
|
|
// 2. Remove LocalizationTexts (NO ACTION FK - won't cascade)
|
|
|
|
|
_dbContext.RemoveRange(novel.Name.Texts);
|
|
|
|
|
_dbContext.RemoveRange(novel.Description.Texts);
|
|
|
|
|
foreach (var chapter in novel.Chapters)
|
|
|
|
|
foreach (var volume in novel.Volumes)
|
|
|
|
|
{
|
|
|
|
|
_dbContext.RemoveRange(chapter.Name.Texts);
|
|
|
|
|
_dbContext.RemoveRange(chapter.Body.Texts);
|
|
|
|
|
_dbContext.RemoveRange(volume.Name.Texts);
|
|
|
|
|
foreach (var chapter in volume.Chapters)
|
|
|
|
|
{
|
|
|
|
|
_dbContext.RemoveRange(chapter.Name.Texts);
|
|
|
|
|
_dbContext.RemoveRange(chapter.Body.Texts);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Remove Images (NO ACTION FK - won't cascade)
|
|
|
|
|
if (novel.CoverImage != null)
|
|
|
|
|
_dbContext.Images.Remove(novel.CoverImage);
|
|
|
|
|
foreach (var chapter in novel.Chapters)
|
|
|
|
|
_dbContext.Images.RemoveRange(chapter.Images);
|
|
|
|
|
foreach (var volume in novel.Volumes)
|
|
|
|
|
{
|
|
|
|
|
foreach (var chapter in volume.Chapters)
|
|
|
|
|
_dbContext.Images.RemoveRange(chapter.Images);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Remove novel - cascades: chapters, localization keys, tag mappings
|
|
|
|
|
// 4. Remove novel - cascades: volumes, chapters, localization keys, tag mappings
|
|
|
|
|
_dbContext.Novels.Remove(novel);
|
|
|
|
|
await _dbContext.SaveChangesAsync();
|
|
|
|
|
}
|
|
|
|
|
|