Some checks failed
Release / build-and-push (map[dockerfile:FictionArchive.API/Dockerfile name:api]) (pull_request) Has been cancelled
Release / build-and-push (map[dockerfile:FictionArchive.Service.AuthenticationService/Dockerfile name:authentication-service]) (pull_request) Has been cancelled
Release / build-and-push (map[dockerfile:FictionArchive.Service.FileService/Dockerfile name:file-service]) (pull_request) Has been cancelled
Release / build-and-push (map[dockerfile:FictionArchive.Service.NovelService/Dockerfile name:novel-service]) (pull_request) Has been cancelled
Release / build-and-push (map[dockerfile:FictionArchive.Service.SchedulerService/Dockerfile name:scheduler-service]) (pull_request) Has been cancelled
Release / build-and-push (map[dockerfile:FictionArchive.Service.TranslationService/Dockerfile name:translation-service]) (pull_request) Has been cancelled
Release / build-and-push (map[dockerfile:FictionArchive.Service.UserService/Dockerfile name:user-service]) (pull_request) Has been cancelled
Release / build-frontend (pull_request) Has been cancelled
Build Gateway / build-subgraphs (map[name:novel-service project:FictionArchive.Service.NovelService subgraph:Novel]) (pull_request) Failing after 51s
Build Gateway / build-subgraphs (map[name:translation-service project:FictionArchive.Service.TranslationService subgraph:Translation]) (pull_request) Has been cancelled
Build Gateway / build-subgraphs (map[name:user-service project:FictionArchive.Service.UserService subgraph:User]) (pull_request) Has been cancelled
Build Gateway / build-gateway (pull_request) Has been cancelled
Build Gateway / build-subgraphs (map[name:scheduler-service project:FictionArchive.Service.SchedulerService subgraph:Scheduler]) (pull_request) Has been cancelled
CI / build-frontend (pull_request) Has been cancelled
CI / build-backend (pull_request) Has been cancelled
227 lines
8.8 KiB
C#
227 lines
8.8 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.IntegrationEvents;
|
|
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();
|
|
}
|
|
|
|
public async Task<NovelUpdateRequestedEvent> QueueNovelImport(string novelUrl)
|
|
{
|
|
var importNovelRequestEvent = new NovelUpdateRequestedEvent()
|
|
{
|
|
NovelUrl = novelUrl
|
|
};
|
|
await _eventBus.Publish(importNovelRequestEvent);
|
|
return importNovelRequestEvent;
|
|
}
|
|
|
|
public async Task<ChapterPullRequestedEvent> QueueChapterPull(uint novelId, uint chapterNumber)
|
|
{
|
|
var chapterPullEvent = new ChapterPullRequestedEvent()
|
|
{
|
|
NovelId = novelId,
|
|
ChapterNumber = chapterNumber
|
|
};
|
|
await _eventBus.Publish(chapterPullEvent);
|
|
return chapterPullEvent;
|
|
}
|
|
}
|