Files
FictionArchive/FictionArchive.Service.NovelService/Services/NovelUpdateService.cs

205 lines
8.0 KiB
C#

using FictionArchive.Service.FileService.IntegrationEvents;
using FictionArchive.Service.NovelService.Models.Configuration;
using FictionArchive.Service.NovelService.Models.Enums;
using FictionArchive.Service.NovelService.Models.Images;
using FictionArchive.Service.NovelService.Models.Localization;
using FictionArchive.Service.NovelService.Models.Novels;
using FictionArchive.Service.NovelService.Models.SourceAdapters;
using FictionArchive.Service.NovelService.Services.SourceAdapters;
using FictionArchive.Service.Shared.Services.EventBus;
using HtmlAgilityPack;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace FictionArchive.Service.NovelService.Services;
public class NovelUpdateService
{
private readonly NovelServiceDbContext _dbContext;
private readonly ILogger<NovelUpdateService> _logger;
private readonly IEnumerable<ISourceAdapter> _sourceAdapters;
private readonly IEventBus _eventBus;
private readonly NovelUpdateServiceConfiguration _novelUpdateServiceConfiguration;
public NovelUpdateService(NovelServiceDbContext dbContext, ILogger<NovelUpdateService> logger, IEnumerable<ISourceAdapter> sourceAdapters, IEventBus eventBus, IOptions<NovelUpdateServiceConfiguration> novelUpdateServiceConfiguration)
{
_dbContext = dbContext;
_logger = logger;
_sourceAdapters = sourceAdapters;
_eventBus = eventBus;
_novelUpdateServiceConfiguration = novelUpdateServiceConfiguration.Value;
}
public async Task<Novel> ImportNovel(string novelUrl)
{
NovelMetadata? metadata = null;
foreach (ISourceAdapter sourceAdapter in _sourceAdapters)
{
if (await sourceAdapter.CanProcessNovel(novelUrl))
{
metadata = await sourceAdapter.GetMetadata(novelUrl);
}
}
if (metadata == null)
{
throw new NotSupportedException("The provided novel url is currently unsupported.");
}
var systemTags = metadata.SystemTags.Select(tag => new NovelTag()
{
Key = tag,
DisplayName = LocalizationKey.CreateFromText(tag, metadata.RawLanguage),
TagType = TagType.System
});
var sourceTags = metadata.SourceTags.Select(tag => new NovelTag()
{
Key = tag,
DisplayName = LocalizationKey.CreateFromText(tag, metadata.RawLanguage),
TagType = TagType.External
});
var addedNovel = _dbContext.Novels.Add(new Novel()
{
Author = new Person()
{
Name = LocalizationKey.CreateFromText(metadata.AuthorName, metadata.RawLanguage),
ExternalUrl = metadata.AuthorUrl,
},
RawLanguage = metadata.RawLanguage,
Url = metadata.Url,
ExternalId = metadata.ExternalId,
CoverImage = metadata.CoverImage != null ? new Image()
{
OriginalPath = metadata.CoverImage.Url,
} : null,
Chapters = metadata.Chapters.Select(chapter =>
{
return new Chapter()
{
Order = chapter.Order,
Url = chapter.Url,
Revision = chapter.Revision,
Name = LocalizationKey.CreateFromText(chapter.Name, metadata.RawLanguage),
Body = new LocalizationKey()
{
Texts = new List<LocalizationText>()
}
};
}).ToList(),
Description = LocalizationKey.CreateFromText(metadata.Description, metadata.RawLanguage),
Name = LocalizationKey.CreateFromText(metadata.Name, metadata.RawLanguage),
RawStatus = metadata.RawStatus,
Tags = sourceTags.Concat(systemTags).ToList(),
Source = new Source()
{
Name = metadata.SourceDescriptor.Name,
Url = metadata.SourceDescriptor.Url,
Key = metadata.SourceDescriptor.Key,
}
});
await _dbContext.SaveChangesAsync();
// Signal request for cover image if present
if (addedNovel.Entity.CoverImage != null)
{
await _eventBus.Publish(new FileUploadRequestCreatedEvent()
{
RequestId = addedNovel.Entity.CoverImage.Id,
FileData = metadata.CoverImage.Data,
FilePath = $"Novels/{addedNovel.Entity.Id}/Images/cover.jpg"
});
}
return addedNovel.Entity;
}
public async Task<Chapter> PullChapterContents(uint novelId, uint chapterNumber)
{
var novel = await _dbContext.Novels.Where(novel => novel.Id == novelId)
.Include(novel => novel.Chapters)
.ThenInclude(chapter => chapter.Body)
.ThenInclude(body => body.Texts)
.Include(novel => novel.Source).Include(novel => novel.Chapters).ThenInclude(chapter => chapter.Images)
.FirstOrDefaultAsync();
var chapter = novel.Chapters.Where(chapter => chapter.Order == chapterNumber).FirstOrDefault();
var adapter = _sourceAdapters.FirstOrDefault(adapter => adapter.SourceDescriptor.Key == novel.Source.Key);
var rawChapter = await adapter.GetRawChapter(chapter.Url);
var localizationText = new LocalizationText()
{
Text = rawChapter.Text,
Language = novel.RawLanguage
};
chapter.Body.Texts.Add(localizationText);
chapter.Images = rawChapter.ImageData.Select(img => new Image()
{
OriginalPath = img.Url
}).ToList();
await _dbContext.SaveChangesAsync();
// Images are saved and have ids, update the chapter body to replace image tags
var chapterDoc = new HtmlDocument();
chapterDoc.LoadHtml(rawChapter.Text);
foreach (var image in chapter.Images)
{
var match = chapterDoc.DocumentNode.SelectSingleNode(@$"//img[@src='{image.OriginalPath}']");
if (match != null)
{
match.Attributes["src"].Value = _novelUpdateServiceConfiguration.PendingImageUrl;
if (match.Attributes.Contains("alt"))
{
match.Attributes["alt"].Value = image.Id.ToString();
}
else
{
match.Attributes.Add("alt", image.Id.ToString());
}
}
}
localizationText.Text = chapterDoc.DocumentNode.OuterHtml;
await _dbContext.SaveChangesAsync();
// Body was updated, raise image request
int imgCount = 0;
foreach (var image in chapter.Images)
{
var data = rawChapter.ImageData.FirstOrDefault(img => img.Url == image.OriginalPath);
await _eventBus.Publish(new FileUploadRequestCreatedEvent()
{
FileData = data.Data,
FilePath = $"{novel.Id}/Images/Chapter-{chapter.Id}/{imgCount++}.jpg",
RequestId = image.Id
});
}
return chapter;
}
public async Task UpdateImage(Guid imageId, string newUrl)
{
var image = await _dbContext.Images
.Include(img => img.Chapter)
.ThenInclude(chapter => chapter.Body)
.ThenInclude(body => body.Texts)
.FirstOrDefaultAsync(image => image.Id == imageId);
image.NewPath = newUrl;
// If this is an image from a chapter, let's update the chapter body(s)
if (image.Chapter != null)
{
foreach (var bodyText in image.Chapter.Body.Texts)
{
var chapterDoc = new HtmlDocument();
chapterDoc.LoadHtml(bodyText.Text);
var match = chapterDoc.DocumentNode.SelectSingleNode(@$"//img[@alt='{image.Id}']");
if (match != null)
{
match.Attributes["src"].Value = newUrl;
}
}
}
await _dbContext.SaveChangesAsync();
}
}