Add project files.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
using aeqw89.DataKeys;
|
||||
using HtmlAgilityPack;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Beam.Temporary.Cli {
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// A collection of specific useful methods and constants that facilitate the use of the application; allows other parts of the application to depend on architecture-specific arbitrary choices without compromising the Single-Responsibility principle or increasing redundant code.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
partial interface IArchitecture {
|
||||
/// <summary>
|
||||
/// Gets the metadata associated with a <see cref="TextResource"/>
|
||||
/// </summary>
|
||||
/// <param name="web">The web client to use when downloading <see cref="WebResource"/>s</param>
|
||||
/// <param name="pieceKey">The key of the <see cref="TextResource"/> stored in the <paramref name="sdd"/></param>
|
||||
/// <param name="sdd">The <see cref="SharedDataDictionary"/> to be used to retrieve information</param>
|
||||
/// <param name="logger">Optional logger for logging debug information</param>
|
||||
/// <returns>A <see cref="DownloadContext{T}"/> object with the required information to perform the download</returns>
|
||||
public DownloadContext<IDocumentMetaData>? GetMeta(HtmlWeb web, DataKey<TextResource> pieceKey, SharedDataDictionary sdd, ILogger? logger = null);
|
||||
/// <summary>
|
||||
/// Gets the <see cref="DownloadContext{T}"/> of the text record associated with <see cref="TextResource"/>
|
||||
/// </summary>
|
||||
/// <param name="web">The web client to use when downloading <see cref="WebResource"/>s</param>
|
||||
/// <param name="pieceKey">The key of the <see cref="TextResource"/> stored in the <paramref name="sdd"/></param>
|
||||
/// <param name="sdd">The <see cref="SharedDataDictionary"/> to be used to retrieve information</param>
|
||||
/// <param name="metadata">Optional book metadata to include with the final text record</param>
|
||||
/// <param name="logger">Optional logger for logging debug information</param>
|
||||
/// <returns>A <see cref="DownloadContext{T}"/> object with the required information to perform the download</returns>
|
||||
public DownloadContext<IDocument>? GetTextRecord(HtmlWeb web, DataKey<TextResource> pieceKey, SharedDataDictionary sdd, IDocumentMetaData? metadata = null, ILogger? logger = null);
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DataKey{IDocumentMetaData}"/> to use when looking for the chapter metadata
|
||||
/// </summary>
|
||||
public DataKey<IDocumentMetaData> ChapterKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DataKey{IDocumentMetaData}"/> to use when looking for the book metadata
|
||||
/// </summary>
|
||||
public DataKey<IDocumentMetaData> BookKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default architecture
|
||||
/// </summary>
|
||||
public static IArchitecture Default => new MainArchitecture();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.1" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.49.2-preview.0.70" />
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Beam.Dynamic\Beam.Dynamic.csproj" />
|
||||
<ProjectReference Include="..\Beam.Exports\Beam.Exports.csproj" />
|
||||
<ProjectReference Include="..\Beam\Beam.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="aeqw89.DataKeys">
|
||||
<HintPath>..\..\aeqw89.DataKeys\aeqw89.DataKeys\bin\Debug\net9.0\aeqw89.DataKeys.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="aeqw89.PersistentData">
|
||||
<HintPath>..\..\aeqw89.PersistentData\aeqw89.PersistentData\bin\Release\net9.0\aeqw89.PersistentData.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace Beam.Temporary.Cli {
|
||||
public class CssData {
|
||||
// Primary background color (e.g., for the body)
|
||||
public string PrimaryColor { get; set; } = "#f5f5f5";
|
||||
|
||||
// Secondary color (e.g., for header background)
|
||||
public string SecondaryColor { get; set; } = "#e0e0e0";
|
||||
|
||||
// Tertiary color (e.g., for content sections)
|
||||
public string TertiaryColor { get; set; } = "#ffffff";
|
||||
|
||||
// Button background color
|
||||
public string ButtonColor { get; set; } = "#007bff";
|
||||
|
||||
// Foreground text color
|
||||
public string ForegroundColor { get; set; } = "#333333";
|
||||
|
||||
// Font family for main content
|
||||
public string ContentFont { get; set; } = "Arial, sans-serif";
|
||||
|
||||
// Font size for main content
|
||||
public string ContentFontSize { get; set; } = "16px";
|
||||
|
||||
// Font family for titles
|
||||
public string TitleFont { get; set; } = "Georgia, serif";
|
||||
|
||||
// Font size for titles
|
||||
public string TitleFontSize { get; set; } = "32px";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
|
||||
using aeqw89.DataKeys;
|
||||
|
||||
namespace Beam.Temporary.Cli {
|
||||
internal static class DataKeyExtensions {
|
||||
public static DataKey WithNamespace(this DataKey dk, string @namespace) {
|
||||
string[] names = @namespace.Split(':');
|
||||
var agg = (string x, string y) => $"{x}:{y}";
|
||||
for (int i = 0; i < names.Length; i++) {
|
||||
string test = names.SkipLast(i).Aggregate(agg);
|
||||
if (dk.Identifier.StartsWith(test)) {
|
||||
return new DataKey(dk.Identifier.Replace(test, @namespace));
|
||||
}
|
||||
}
|
||||
|
||||
return new DataKey(@namespace + ":" + dk.Identifier);
|
||||
}
|
||||
|
||||
public static DataKey<T> WithNamespace<T>(this DataKey<T> dk, string @namespace) {
|
||||
return ((DataKey)dk).WithNamespace(@namespace).As<T>();
|
||||
}
|
||||
|
||||
public static DataKey<T> WithSuffix<T>(this DataKey<T> dk, string suffix) {
|
||||
return new DataKey<T>(dk.Identifier + suffix);
|
||||
}
|
||||
|
||||
public static DataKey ToAggregator(this DataKey dk)
|
||||
=> dk.WithNamespace("aeqw89:document:aggregators");
|
||||
public static DataKey ToAuxiliary(this DataKey dk)
|
||||
=> dk.WithNamespace("aeqw89:document:auxillaries");
|
||||
public static DataKey<T> As<T>(this DataKey dk) => new DataKey<T>(dk.Identifier);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Beam.Temporary.Cli {
|
||||
internal class File(string path, params string[] tags) {
|
||||
public string Path { get; set; } = path;
|
||||
public string[] Tags { get; set; } = tags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//using aeqw89.DataKeys;
|
||||
//using System;
|
||||
//using System.Collections.Generic;
|
||||
//using System.Linq;
|
||||
//using System.Text;
|
||||
//using System.Threading.Tasks;
|
||||
|
||||
//namespace Beam.Temporary.Cli {
|
||||
// internal class HtmlBook : Document {
|
||||
// public class Keys {
|
||||
// public static DataKey<File> ContentPage => new DataKey<File>("content_page");
|
||||
// public static DataKey<File> NoContentPage => new DataKey<File>("no_content_page");
|
||||
// public static DataKey<File> TitlePage => new DataKey<File>("title_page");
|
||||
// public static DataKey<File> StylesPage => new DataKey<File>("styles_page");
|
||||
// }
|
||||
|
||||
// public List<Tracked<IDocument>> Documents { get; set; }
|
||||
// public IReadOnlyList<string> Pages => _Pages;
|
||||
// private List<string> _Pages { get; set; } = [];
|
||||
|
||||
// private const string EMTPY_PAGE = "EMPTY";
|
||||
|
||||
// public CssData CssData { get; }
|
||||
// public ArticleData BookData { get; set; }
|
||||
// public HtmlBookTemplates Templates { get; set; }
|
||||
|
||||
// public HtmlBook(string bookname, CssData cssData, ArticleData bookData, HtmlBookTemplates templates, List<IDocument>? documents = null, Encoding? encoding = null)
|
||||
// : base(bookname, encoding) {
|
||||
// Documents = [];
|
||||
// CssData = cssData;
|
||||
// BookData = bookData;
|
||||
// Templates = templates;
|
||||
// if (documents is not null)
|
||||
// Documents = documents.Select((x) => new Tracked<IDocument>(x)).ToList();
|
||||
// }
|
||||
|
||||
// public void Update(bool ignoreDirty = false) {
|
||||
// if (!Directory.Exists(Filename))
|
||||
// Directory.CreateDirectory(Filename);
|
||||
|
||||
// //System.IO.File.WriteAllLines(Path.Combine(Filename, "styles.css"), Format())
|
||||
|
||||
// List<string> newpages = [];
|
||||
// if (Pages.Count < Documents.Count)
|
||||
// _Pages.AddRange(Enumerable.Repeat(EMTPY_PAGE, Documents.Count - Pages.Count));
|
||||
// foreach (var (doc, page) in Documents.Zip(Pages)) {
|
||||
// if (!doc.IsDirty)
|
||||
// newpages.Add(page);
|
||||
// else if (doc.TrackedObject.MetaData.Count == 0)
|
||||
// newpages.Add(PlainPage(doc.TrackedObject));
|
||||
// else if (doc.TrackedObject.MetaData.TryGetValue(Program.Architecture.ChapterKey, out var meta) && meta is ArticleData articleData)
|
||||
// newpages.Add(ArticlePage(doc.TrackedObject, articleData));
|
||||
// else {
|
||||
// Console.WriteLine("Unhandlable Metadata detected!");
|
||||
// newpages.Add(PlainPage(doc.TrackedObject));
|
||||
// }
|
||||
|
||||
// System.IO.File.WriteAllText(Path.Combine(Filename, Path.GetRandomFileName() + ".html"), newpages[^1]);
|
||||
// doc.IsDirty = false;
|
||||
// }
|
||||
|
||||
// _Pages = newpages;
|
||||
// }
|
||||
|
||||
// public void UpdateCss() {
|
||||
|
||||
// }
|
||||
|
||||
// public void UpateTitle() {
|
||||
|
||||
// }
|
||||
|
||||
// private string Format(string template, Dictionary<string, string> table) {
|
||||
// ArgumentNullException.ThrowIfNull(template);
|
||||
// ArgumentNullException.ThrowIfNull(table);
|
||||
|
||||
// foreach (var kvp in table) {
|
||||
// template = template.Replace(kvp.Key, kvp.Value);
|
||||
// }
|
||||
// return template;
|
||||
// }
|
||||
|
||||
// private Dictionary<string, string> GetDocumentTable(IDocument doc, bool keepPlaceholders = false) {
|
||||
// var table = new Dictionary<string, string>() {
|
||||
// { "{" + nameof(doc.Filename) + "}", doc.Filename },
|
||||
// { "{Content}", doc.ToString() }
|
||||
// };
|
||||
|
||||
// return SolvePlaceholders(table, keepPlaceholders);
|
||||
// }
|
||||
|
||||
// private Dictionary<string, string> GetArticleDataTable(IDocument doc, ArticleData ad, bool keepPlaceholders = false) {
|
||||
// var table = new Dictionary<string, string>() {
|
||||
// { "{" + nameof(ad.Language) + "}", ad.Language ?? "" },
|
||||
// { "{" + nameof(ad.Authors) + "}", ad.Authors.Aggregate("; ")},
|
||||
// { "{" + nameof(ad.Categories) + "}", ad.Categories.Aggregate("; ") },
|
||||
// { "{" + nameof(ad.Version) + "}", ad.Version ?? "" },
|
||||
// { "{" + nameof(ad.Description) + "}", ad.Description ?? "" },
|
||||
// { "{" + nameof(ad.Name) + "}", ad.Name ?? "" },
|
||||
// { "{" + nameof(doc.Filename) + "}", doc.Filename },
|
||||
// { "{Content}", doc.ToString() }
|
||||
// };
|
||||
|
||||
// return SolvePlaceholders(table, keepPlaceholders);
|
||||
// }
|
||||
|
||||
// private Dictionary<string, string> SolvePlaceholders(Dictionary<string, string> table, bool keepPlaceholders) {
|
||||
// if (keepPlaceholders)
|
||||
// return table.Select(
|
||||
// (x) => new KeyValuePair<string, string>(x.Key, x.Value == "" ? $"{x.Key}" : x.Value))
|
||||
// .ToDictionary();
|
||||
// return table;
|
||||
// }
|
||||
|
||||
// private string PlainPage(IDocument doc, bool keepPlaceholders = false) {
|
||||
// return Format(Templates.ContentPageTemplate, GetDocumentTable(doc, keepPlaceholders));
|
||||
// }
|
||||
|
||||
// private string ArticlePage(IDocument doc, ArticleData data, bool keepPlaceholders = false) {
|
||||
// return Format(Templates.ContentPageTemplate, GetArticleDataTable(doc, data, keepPlaceholders));
|
||||
// }
|
||||
|
||||
// public override byte[] ToBytes() {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
|
||||
// public override string ToString() {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Beam.Temporary.Cli {
|
||||
internal struct HtmlBookTemplates {
|
||||
public string TitlePageTemplate { get; set; }
|
||||
public string ContentPageTemplate { get; set; }
|
||||
public string CssTemplate { get; set; }
|
||||
public string NoContentTemplate { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using aeqw89.DataKeys;
|
||||
using Beam.Dynamic;
|
||||
using HtmlAgilityPack;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Beam.Temporary.Cli {
|
||||
partial interface IArchitecture {
|
||||
private class MainArchitecture : IArchitecture {
|
||||
public MainArchitecture() { }
|
||||
|
||||
public DataKey<IDocumentMetaData> ChapterKey { get; set; } = new("ma:chapter");
|
||||
public DataKey<IDocumentMetaData> BookKey { get; set; } = new("ma:book");
|
||||
|
||||
public DownloadContext<IDocumentMetaData>? GetMeta(HtmlWeb web, DataKey<TextResource> pieceKey, SharedDataDictionary sdd, ILogger? logger = null) {
|
||||
var piece = sdd.Novels[pieceKey].ToRecord(sdd); // retrieves novel data from the sdd
|
||||
var auxiliary = piece.AssociatedMetaSource?.ToRecord(sdd); // retrieves novel aux data from the sdd
|
||||
|
||||
// null checks
|
||||
if (auxiliary is null) // aux is required to get metadata
|
||||
return null;
|
||||
if (piece?.Resource?.MetaTemplateInitialData is null) // sanity check to avoid null warnings
|
||||
return null;
|
||||
|
||||
// gets the link for the novel's metadata using the auxillary data retrieved from the sdd
|
||||
var link = sdd.Templates[auxiliary.Resource.Key].GenerateLink(piece?.Resource?.MetaTemplateInitialData!);
|
||||
var binding = auxiliary.Bindings;
|
||||
|
||||
return new DownloadContext<IDocumentMetaData>(web, [link], downloadLogger: logger, transformer: (x) => {
|
||||
return new ArticleData() {
|
||||
Authors = [OnlineCleaner.Clean(binding?.Authors?.Resolve(x) ?? "")],
|
||||
Name = OnlineCleaner.Clean(binding?.Title?.ResolveString(x) ?? ""),
|
||||
Categories = OnlineCleaner.Clean(binding?.Tags?.ResolveString(x) ?? "").Split(';') ?? [],
|
||||
Description = OnlineCleaner.Clean(binding?.Description?.ResolveString(x) ?? "")
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public DownloadContext<IDocument>? GetTextRecord(HtmlWeb web, DataKey<TextResource> resKey, SharedDataDictionary sdd, IDocumentMetaData? metaData = null, ILogger? logger = null) {
|
||||
var res = sdd.Novels[resKey].ToRecord(sdd); // retrieves the novel data from the sdd
|
||||
var aggregator = res.AssociatedSource?.ToRecord(sdd); // retrieves the aggregator (novel web source) from the sdd
|
||||
|
||||
if (aggregator is null) // ensure aggergator data was retrieved successfully
|
||||
return null;
|
||||
if (res is null) // ensure novel data was retrieved successfully
|
||||
return null;
|
||||
|
||||
var template = sdd.Templates[aggregator.Resource.Key]; // gets the link generator for the specified aggregator
|
||||
|
||||
// creates a generative enumerable of type link from 'template'
|
||||
var sle = SourceLinkEnumerable.FromGenerator(new DataBackedSourceLinkGenerator(
|
||||
template, res.Resource.TemplateInitialData));
|
||||
|
||||
return new DownloadContext<IDocument>(web, sle,
|
||||
transformer: (x) => {
|
||||
var resolved = aggregator.Bindings.Resolve(x);
|
||||
var articleData = new ArticleData() {
|
||||
Name = OnlineCleaner.Clean(resolved.Title),
|
||||
};
|
||||
Dictionary<DataKey<IDocumentMetaData>, IDocumentMetaData> meta = [];
|
||||
meta.Add(ChapterKey, articleData);
|
||||
if (metaData is not null)
|
||||
meta.Add(BookKey, metaData);
|
||||
return new StringDocument(Path.GetRandomFileName(), OnlineCleaner.Clean(resolved.Content)) {
|
||||
MetaData = meta
|
||||
};
|
||||
},
|
||||
retryReporter: new Progress<int>((x) => Console.WriteLine($"Retrying download ({x})")),
|
||||
downloadReporter: new Progress<IDocument>((x) => Console.WriteLine($"Downloaded ({x.Filename})")),
|
||||
asyncFailurePredicates: [
|
||||
(x) => Task.FromResult(!x.DocumentNode.InnerHtml.Contains("<div id=\"chapter-container\" class=\"chapter-content\" itemprop=\"description\">"))
|
||||
],
|
||||
timeOut: TimeSpan.FromSeconds(15),
|
||||
downloadLogger: logger
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
|
||||
|
||||
using aeqw89.DataKeys;
|
||||
using Beam.Dynamic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Beam.Temporary.Cli {
|
||||
|
||||
internal static class NovelStatics {
|
||||
public static void Define_LightNovelWorld_Novel_TheLegendaryMechanic(SharedDataDictionary sdd) {
|
||||
var lnwAggregator = new DataKey<WebResource>("aeqw89:document:aggregators:light_novel_world");
|
||||
var lnwAuxiliary = new DataKey<WebResource>("aeqw89:document:auxillaries:light_novel_world");
|
||||
var novel = new TextResource() {
|
||||
Key = new DataKey<TextResource>("novels:the_legendary_mechanic"),
|
||||
AssociatedSource = lnwAggregator,
|
||||
AssociatedMetaSource = lnwAuxiliary,
|
||||
TemplateInitialData = ["the-legendary-mechanic-245", "1"],
|
||||
MetaTemplateInitialData = ["the-legendary-mechanic"]
|
||||
};
|
||||
sdd.Novels.TryAdd(novel.Key, novel);
|
||||
|
||||
sdd.AggregatorNovels.TryAdd(lnwAggregator, [novel.Key]);
|
||||
}
|
||||
|
||||
public static void Define_LightNovelWorl_Novel_IAloneLevelUp(SharedDataDictionary sdd) {
|
||||
var lnwAggregator = new DataKey("light_novel_world").ToAggregator().As<WebResource>();
|
||||
var lnwAuxiliary = new DataKey("light_novel_world").ToAuxiliary().As<WebResource>();
|
||||
var novel = new TextResource() {
|
||||
Key = new DataKey<TextResource>("novels:i_alone_level_up"),
|
||||
AssociatedSource = lnwAggregator,
|
||||
AssociatedMetaSource = lnwAuxiliary,
|
||||
TemplateInitialData = ["i-alone-level-up-236", "1"],
|
||||
MetaTemplateInitialData = ["i-alone-level-up-solo-leveling-05122225"]
|
||||
};
|
||||
|
||||
sdd.Novels.TryAdd(novel.Key, novel);
|
||||
|
||||
sdd.AggregatorNovels.TryAdd(lnwAggregator, [novel.Key]);
|
||||
}
|
||||
|
||||
public static void Define_NovelFull(SharedDataDictionary sdd) {
|
||||
var docNamespace = "aeqw89:document";
|
||||
var nfAgg = new DataKey<WebResource>("aggregators:novel_full").WithNamespace(docNamespace);
|
||||
var nfAux = new DataKey<WebResource>("auxillaries:novel_full").WithNamespace(docNamespace);
|
||||
var nfBindings = new DataKey<DataBindings>("aeqw89:bindings:light_novel_world");
|
||||
var aggregator = new WebResource(nfAgg) {
|
||||
Name = "Novel Full",
|
||||
Description = "A novel aggregator site",
|
||||
Domain = "https://novelfull.net",
|
||||
Bindings = nfBindings
|
||||
};
|
||||
var auxiliary = new WebResource(nfAux) {
|
||||
Name = "Novel Full",
|
||||
Description = "A novel aggregator site",
|
||||
Domain = "https://novelfull.net",
|
||||
Bindings = nfBindings.WithSuffix("_aux")
|
||||
};
|
||||
|
||||
sdd.Templates.TryAdd(nfAgg, new() {
|
||||
Template = ""
|
||||
});
|
||||
}
|
||||
|
||||
public static void Define_LightNovelWorld(SharedDataDictionary sdd) {
|
||||
var lnwAggregator = new DataKey<WebResource>("aeqw89:document:aggregators:light_novel_world");
|
||||
var lnwAuxiliary = new DataKey<WebResource>("aeqw89:document:auxillaries:light_novel_world");
|
||||
const string lnwBindingsA = "aeqw89:bindings:light_novel_world";
|
||||
var aggregator = new WebResource(lnwAggregator) {
|
||||
Name = "Light Novel World",
|
||||
Description = "A novel aggregator site maintained by NetherClaw",
|
||||
Domain = "https://www.lightnovelworld.co",
|
||||
Bindings = new DataKey<DataBindings>(lnwBindingsA)
|
||||
};
|
||||
const string lnwBindingsB = "aeqw89:bindings:light_novel_world_aux";
|
||||
var auxiliary = new WebResource(lnwAuxiliary) {
|
||||
Name = "Light Novel World",
|
||||
Description = "A novel aggregator site maintained by NetherClaw",
|
||||
Domain = "https://www.lightnovelworld.co",
|
||||
Bindings = new DataKey<DataBindings>(lnwBindingsB)
|
||||
};
|
||||
|
||||
sdd.Templates.TryAdd(lnwAuxiliary, new() {
|
||||
Template = "https://www.lightnovelworld.co/novel/{0}",
|
||||
IndexOfChapterIndex = -1
|
||||
});
|
||||
sdd.Templates.TryAdd(lnwAggregator, new() {
|
||||
Template = "https://www.lightnovelworld.co/novel/{0}/chapter-{1}",
|
||||
IndexOfChapterIndex = 1
|
||||
});
|
||||
|
||||
sdd.Aggregators.TryAdd(aggregator.Key, aggregator);
|
||||
sdd.Auxillaries.TryAdd(auxiliary.Key, auxiliary);
|
||||
|
||||
var lnwBindings = new DataKey<DataBindings>(lnwBindingsA);
|
||||
var lnwBindingsAux = new DataKey<DataBindings>(lnwBindingsB);
|
||||
sdd.Bindings.TryAdd(lnwBindings, new DataBindings() {
|
||||
Title = new Binding("aeqw89:binding:light_novel_world:title") {
|
||||
XPath = "/html/body/main/article/section/div[1]/h1/span[2]",
|
||||
Type = BindingType.Single
|
||||
},
|
||||
Content = new("aeqw89:binding:light_novel_world:content") {
|
||||
Provider = new ParagraphedContentDataProvider() {
|
||||
Content = new Binding() {
|
||||
XPath = "//*[@id=\"chapter-container\"]"
|
||||
}
|
||||
},
|
||||
Type = BindingType.UseProvider
|
||||
},
|
||||
});
|
||||
sdd.Bindings.TryAdd(lnwBindingsAux, new DataBindings() {
|
||||
Title = new("aeqw89:binding:light_novel_world_aux:title") {
|
||||
XPath = "/html/body/main/article/header/div[2]/div[2]/div[1]/h1",
|
||||
Type = BindingType.Single
|
||||
},
|
||||
Authors = new("aeqw89:binding:light_novel_world_aux:authors") {
|
||||
XPath = "/html/body/main/article/header/div[2]/div[2]/div[1]/div[1]/a",
|
||||
Type = BindingType.Single
|
||||
},
|
||||
Description = new("aeqw89:binding:light_novel_world_aux:description") {
|
||||
Provider = new ParagraphedContentDataProvider() {
|
||||
Content = new() {
|
||||
XPath = "/html/body/main/article/div/section/div[1]/div"
|
||||
}
|
||||
},
|
||||
Type = BindingType.UseProvider
|
||||
},
|
||||
Tags = new("aeqw89:binding:light_novel_world_aux:tags") {
|
||||
Provider = new ListContentDataProvider() {
|
||||
Content = new() {
|
||||
XPath = "/html/body/main/article/header/div[2]/div[2]/div[3]/ul"
|
||||
}
|
||||
},
|
||||
Type = BindingType.UseProvider
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
using aeqw89.PersistentData;
|
||||
using aeqw89.DataKeys;
|
||||
using Beam.Dynamic;
|
||||
using HtmlAgilityPack;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Beam.Temporary.Cli.Templates.Classic;
|
||||
using Beam.Exports;
|
||||
|
||||
namespace Beam.Temporary.Cli {
|
||||
internal class Program {
|
||||
|
||||
public static JsonSerializerOptions ConversionOptions { get; internal set; } = new();
|
||||
|
||||
public static SharedDataDictionary Shared { get; set; } = [];
|
||||
|
||||
public static IArchitecture Architecture = IArchitecture.Default;
|
||||
|
||||
const string SharedDataPath = "data/.dat";
|
||||
|
||||
static async Task Main(string[] args) {
|
||||
ConversionOptions.Converters.AddPersistentDataRequiredConverters();
|
||||
ConversionOptions.WriteIndented = true;
|
||||
|
||||
var web = new HtmlWeb();
|
||||
|
||||
var lf = LoggerFactory.Create((x) => {
|
||||
x.AddConsole();
|
||||
});
|
||||
|
||||
ILogger logger = lf
|
||||
.CreateLogger("Program");
|
||||
|
||||
await using var sharedContext = await DataDictionaryContext<SharedDataDictionary>.Create(
|
||||
SharedDataPath,
|
||||
DataKind.Shared,
|
||||
logger,
|
||||
ConversionOptions
|
||||
);
|
||||
|
||||
Shared = sharedContext.Data;
|
||||
|
||||
Shared.Clear();
|
||||
NovelStatics.Define_LightNovelWorld(Shared);
|
||||
NovelStatics.Define_LightNovelWorld_Novel_TheLegendaryMechanic(Shared);
|
||||
NovelStatics.Define_LightNovelWorl_Novel_IAloneLevelUp(Shared);
|
||||
ClassicTemplates.Register(Shared);
|
||||
|
||||
var novel = new DataKey<TextResource>("novels:i_alone_level_up");
|
||||
var context_aux = Architecture.GetMeta(web, novel, Shared);
|
||||
var metaDownloader = new DownloadEnumerable<IDocumentMetaData>(
|
||||
new SequentialFragmentDownloader<IDocumentMetaData>(
|
||||
context_aux,
|
||||
(c) => new UnitFragmentDownloader<IDocumentMetaData>(c.Web, c.AsyncTranformer, c.AsyncFailurePredicates, 4, logger),
|
||||
logger)
|
||||
.UnwrapFragmented());
|
||||
var metadata = (await metaDownloader.FirstAsync());
|
||||
|
||||
var context = Architecture.GetTextRecord(web, novel, Shared, metadata.Data);
|
||||
context.DownloadReporter = new Progress<IDocument>((x) => Console.WriteLine(x.Filename));
|
||||
var downloader = new DownloadEnumerable<IDocument>(
|
||||
new SequentialFragmentDownloader<IDocument>(
|
||||
context,
|
||||
(c) => new UnitFragmentDownloader<IDocument>(c.Web, c.AsyncTranformer, c.AsyncFailurePredicates, 4, logger),
|
||||
logger)
|
||||
.UnwrapFragmented());
|
||||
|
||||
List<Ordered<IDocument>> documents = [];
|
||||
|
||||
await foreach (var download in downloader.Take(20)) {
|
||||
if (!download.Data.MetaData.TryGetValue(Architecture.ChapterKey, out var meta))
|
||||
continue;
|
||||
if (meta is not ArticleData articleMetaData)
|
||||
continue;
|
||||
//Console.WriteLine($"Title: {data.Name}");
|
||||
//Console.WriteLine($"Description: {data.Description}");
|
||||
//Console.WriteLine($"Categories: {data.Categories.Aggregate((x, y) => $"{x}; {y}")}");
|
||||
//Console.WriteLine($"Authors: {data.Authors.Aggregate((x,y) => $"{x}; {y}")}");
|
||||
Console.WriteLine($"Chapter title: {articleMetaData.Name}");
|
||||
//Console.WriteLine($"Content: {download}");
|
||||
|
||||
documents.Add(download);
|
||||
}
|
||||
|
||||
string testDir = Path.Combine("txt", Path.GetRandomFileName());
|
||||
Directory.CreateDirectory(testDir);
|
||||
|
||||
int len = documents.MaxBy((x) => x.Order)?.Order ?? -1;
|
||||
foreach (var document in documents.OrderBy((x) => x.Order)) {
|
||||
document.Data.MetaData.TryGetValue(Architecture.ChapterKey, out var chapterMetaData);
|
||||
Dictionary<string, string> linkButtons = new();
|
||||
if (document.Order != 0)
|
||||
linkButtons.Add("Previous", $"{document.Order - 1}.html");
|
||||
if (document.Order != len)
|
||||
linkButtons.Add("Next", $"{document.Order + 1}.html");
|
||||
new HtmlExporter(document.Data, chapterMetaData as ArticleData, linkButtons).Write(Path.Combine(testDir, $"{document.Order}.html"));
|
||||
}
|
||||
|
||||
Console.ReadKey();
|
||||
|
||||
//foreach (var download in documents.OrderBy((x) => x.Order)) {
|
||||
// if (download.Data.TryGetTaggedMetaData<ArticleData>(Architecture.ChapterKey, out var meta))
|
||||
// Console.WriteLine($"{download.Order}:{meta.Name}");
|
||||
//}
|
||||
|
||||
//string[] templates = new DataKey<File>[] {
|
||||
// HtmlBook.Keys.ContentPage,
|
||||
// HtmlBook.Keys.NoContentPage,
|
||||
// HtmlBook.Keys.TitlePage,
|
||||
// HtmlBook.Keys.StylesPage,
|
||||
//}.Select(
|
||||
// (x) => Shared.Files.ReadToString(x.WithNamespace("aeqw89:files:templates:classic"))
|
||||
//).ToArray();
|
||||
|
||||
//HtmlBook book = new(
|
||||
// bookname: Path.Combine(Path.GetRandomFileName(), "I Alone Level Up"),
|
||||
// new CssData(),
|
||||
// new ArticleData(),
|
||||
// new HtmlBookTemplates() {
|
||||
// ContentPageTemplate = templates[0],
|
||||
// NoContentTemplate = templates[1],
|
||||
// TitlePageTemplate = templates[2],
|
||||
// CssTemplate = templates[3],
|
||||
// },
|
||||
// documents: documents.Select((x) => x.Data).ToList()
|
||||
//);
|
||||
|
||||
//book.Update();
|
||||
//Console.WriteLine("One variable!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using aeqw89.PersistentData;
|
||||
using aeqw89.DataKeys;
|
||||
using Beam.Dynamic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Beam.Temporary.Cli {
|
||||
public class SharedDataDictionary : BaseDataDictionary {
|
||||
public Dictionary<DataKey<WebResource>, PackagedSourceLinkGenerationData> Templates {
|
||||
get => GetOrCreateDictionary<DataKey<WebResource>, PackagedSourceLinkGenerationData>(nameof(Templates));
|
||||
set => Data[nameof(Templates)] = value;
|
||||
}
|
||||
|
||||
public Dictionary<DataKey<WebResource>, WebResource> Aggregators {
|
||||
get => GetOrCreateDictionary<DataKey<WebResource>, WebResource>(nameof(Aggregators));
|
||||
set => Data[nameof(Aggregators)] = value;
|
||||
}
|
||||
|
||||
public Dictionary<DataKey<WebResource>, WebResource> Auxillaries {
|
||||
get => GetOrCreateDictionary<DataKey<WebResource>, WebResource>(nameof(Auxillaries));
|
||||
set => Data[nameof(Auxillaries)] = value;
|
||||
}
|
||||
|
||||
public Dictionary<DataKey<DataBindings>, DataBindings> Bindings {
|
||||
get => GetOrCreateDictionary<DataKey<DataBindings>, DataBindings>(nameof(Bindings));
|
||||
set => Data[nameof(Bindings)] = value;
|
||||
}
|
||||
|
||||
public Dictionary<DataKey<WebResource>, HashSet<DataKey<TextResource>>> AggregatorNovels {
|
||||
get => GetOrCreateDictionary<DataKey<WebResource>, HashSet<DataKey<TextResource>>>(nameof(AggregatorNovels));
|
||||
set => Data[nameof(AggregatorNovels)] = value;
|
||||
}
|
||||
|
||||
public Dictionary<DataKey<TextResource>, TextResource> Novels {
|
||||
get => GetOrCreateDictionary<DataKey<TextResource>, TextResource>(nameof(Novels));
|
||||
set => Data[nameof(Novels)] = value;
|
||||
}
|
||||
|
||||
internal Dictionary<DataKey<File>, File> Files {
|
||||
get => GetOrCreateDictionary<DataKey<File>, File>(nameof(Files));
|
||||
set => Data[nameof(Files)] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Beam.Temporary.Cli {
|
||||
public static class StringExtensions {
|
||||
public static string Aggregate(this IEnumerable<string> str, string separator) {
|
||||
if (!str.Any())
|
||||
return string.Empty;
|
||||
return str.Aggregate((x, y) => $"{x}{separator}{y}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Beam.Temporary.Cli.Templates.Classic {
|
||||
internal class ClassicTemplates {
|
||||
public static void Register(SharedDataDictionary sdd) {
|
||||
sdd.Files.TryAdd(
|
||||
new("aeqw89:files:templates:classic:content_page"),
|
||||
new("C:\\Users\\qwsdc\\source\\repos\\Beam\\Beam.Temporary.Cli\\Templates\\Classic\\Content.template.html", "htmlpage", "templates"));
|
||||
sdd.Files.TryAdd(
|
||||
new("aeqw89:files:templates:classic:title_page"),
|
||||
new("C:\\Users\\qwsdc\\source\\repos\\Beam\\Beam.Temporary.Cli\\Templates\\Classic\\Title.template.html", "htmlpage", "templates"));
|
||||
sdd.Files.TryAdd(
|
||||
new("aeqw89:files:templates:classic:styles_page"),
|
||||
new("C:\\Users\\qwsdc\\source\\repos\\Beam\\Beam.Temporary.Cli\\Templates\\Classic\\Styles.template.css", "styles", "templates"));
|
||||
sdd.Files.TryAdd(
|
||||
new("aeqw89:files:templates:classic:no_content_page"),
|
||||
new("C:\\Users\\qwsdc\\source\\repos\\Beam\\Beam.Temporary.Cli\\Templates\\Classic\\NoContent.template.html", "htmlpage", "templates"));
|
||||
}
|
||||
}
|
||||
|
||||
internal static class DictionaryOfFileExtensions {
|
||||
public static string ReadToString<T>(this Dictionary<T, File> dict, T key) where T: notnull {
|
||||
return System.IO.File.ReadAllText(dict[key].Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{Name}</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>{Name}</h1>
|
||||
<p><em>{Description}</em></p>
|
||||
<div>
|
||||
<span><strong>Authors:</strong> {Authors}</span> |
|
||||
<span><strong>Language:</strong> {Language}</span> |
|
||||
<span><strong>Categories:</strong> {Categories}</span> |
|
||||
<span><strong>Version:</strong> {Version}</span>
|
||||
</div>
|
||||
</header>
|
||||
<article>
|
||||
{Content}
|
||||
</article>
|
||||
<div class="navigation">
|
||||
<button id="prev">Previous</button>
|
||||
<button id="next">Next</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>404 - Not Found</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1>404 - Content Not Found</h1>
|
||||
<p>The file <strong>{Filename}</strong> was not found.</p>
|
||||
<p>{Content}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
/* styles.css */
|
||||
/* Placeholders:
|
||||
{PrimaryColor}, {SecondaryColor}, {TertiaryColor}, {ButtonColor},
|
||||
{ForegroundColor}, {ContentFont}, {ContentFontSize}, {TitleFont}, {TitleFontSize}
|
||||
*/
|
||||
body {
|
||||
font-family: {ContentFont};
|
||||
font-size: {ContentFontSize};
|
||||
background-color: {PrimaryColor};
|
||||
color: {ForegroundColor};
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: {SecondaryColor};
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-family: {TitleFont};
|
||||
font-size: {TitleFontSize};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-style: italic;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
section, article, nav {
|
||||
background: {TertiaryColor};
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin: 20px auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: {ButtonColor};
|
||||
color: {ForegroundColor};
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
font-size: {ContentFontSize};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
nav h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{Name}</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>{Name}</h1>
|
||||
<p><em>{Description}</em></p>
|
||||
</header>
|
||||
<section>
|
||||
<div><strong>Authors:</strong> {Authors}</div>
|
||||
<div><strong>Language:</strong> {Language}</div>
|
||||
<div><strong>Categories:</strong> {Categories}</div>
|
||||
<div><strong>Version:</strong> {Version}</div>
|
||||
</section>
|
||||
<nav>
|
||||
<h2>Table of Contents</h2>
|
||||
<ul>
|
||||
{TOC} <!-- Expected to be a list of items (e.g. <li>Chapter 1</li>, etc.) -->
|
||||
</ul>
|
||||
</nav>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
|
||||
using aeqw89.DataKeys;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Beam.Temporary.Cli {
|
||||
public class TextResource : IKeyed<TextResource> {
|
||||
public required DataKey<TextResource> Key { get; set; }
|
||||
public DataKey<WebResource>? AssociatedSource { get; set; }
|
||||
public DataKey<WebResource>? AssociatedMetaSource { get; set; }
|
||||
public required string[] TemplateInitialData { get; set; }
|
||||
public string?[]? MetaTemplateInitialData { get; set; }
|
||||
|
||||
public TextResourceRecord ToRecord(SharedDataDictionary sdd) {
|
||||
return new(this,
|
||||
AssociatedSource is null ? null : sdd.Aggregators[AssociatedSource],
|
||||
AssociatedMetaSource is null ? null : sdd.Auxillaries[AssociatedMetaSource]);
|
||||
}
|
||||
}
|
||||
|
||||
public record TextResourceRecord(TextResource Resource, WebResource? AssociatedSource, WebResource? AssociatedMetaSource);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Beam.Temporary.Cli {
|
||||
internal class Tracked<T>(T obj) {
|
||||
public T TrackedObject { get; set; } = obj;
|
||||
public bool IsDirty { get; set; } = true;
|
||||
|
||||
public Tracked<T> SetDirty() {
|
||||
IsDirty = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using aeqw89.PersistentData;
|
||||
using aeqw89.DataKeys;
|
||||
using Beam.Dynamic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Beam.Temporary.Cli {
|
||||
public class WebResource(DataKey<WebResource> key) : IKeyed<WebResource> {
|
||||
public DataKey<WebResource> Key { get; set; } = key;
|
||||
|
||||
public required DataKey<DataBindings> Bindings { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Domain { get; set; }
|
||||
public string? Description { get; set; }
|
||||
|
||||
|
||||
public WebResource() : this(new(string.Empty)) { }
|
||||
|
||||
public WebResourceRecord ToRecord(SharedDataDictionary sdd) {
|
||||
return new WebResourceRecord(this, sdd.Bindings[Bindings]);
|
||||
}
|
||||
}
|
||||
|
||||
public record WebResourceRecord(WebResource Resource, DataBindings Bindings);
|
||||
}
|
||||
Reference in New Issue
Block a user