diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml index b4cd527..79e0e14 100644 --- a/.github/workflows/dotnet-ci.yml +++ b/.github/workflows/dotnet-ci.yml @@ -22,6 +22,11 @@ jobs: with: dotnet-version: 8.x + - name: Prepare + run: | + dotnet nuget add source --username sun128764 --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/MIRIMIRIM/index.json" + dotnet restore + - name: Publish the application run: dotnet publish OKP.Core --configuration ${{ matrix.configuration }} --runtime ${{ matrix.runtime-identifier }} /p:PublishAot=false diff --git a/OKP.Core/Interface/Acgnx/AcgnxAdapter.cs b/OKP.Core/Interface/Acgnx/AcgnxAdapter.cs index 84ea01f..b5566b1 100644 --- a/OKP.Core/Interface/Acgnx/AcgnxAdapter.cs +++ b/OKP.Core/Interface/Acgnx/AcgnxAdapter.cs @@ -186,23 +186,7 @@ private bool Valid() throw new ArgumentNullException(nameof(torrent.Data.TorrentObject)); } - if (template.Content != null && template.Content.ToLower().EndsWith(".html")) - { - Log.Debug("开始寻找{Site} .html文件 {File}", site, template.Content); - var templateFile = FileHelper.ParseFileFullPath(template.Content, torrent.SettingPath); - if (File.Exists(templateFile)) - { - Log.Debug("找到了{Site} .html文件 {File}", site, template.Content); - template.Content = File.ReadAllText(templateFile); - } - else - { - Log.Error("发布模板看起来是个.html文件,但是这个.html文件不存在{NewLine}" + - "{Source}-->{Dest}", Environment.NewLine, template.Content, templateFile); - return false; - } - } - return true; + return ValidTemplate(template, site, torrent.SettingPath); } } diff --git a/OKP.Core/Interface/Acgrip/AcgripAdapter.cs b/OKP.Core/Interface/Acgrip/AcgripAdapter.cs index dfee098..841c790 100644 --- a/OKP.Core/Interface/Acgrip/AcgripAdapter.cs +++ b/OKP.Core/Interface/Acgrip/AcgripAdapter.cs @@ -154,23 +154,7 @@ private bool Valid() return false; } } - if (template.Content != null && template.Content.ToLower().EndsWith(".bbcode")) - { - Log.Debug("开始寻找{Site} bbcode文件 {File}", site, template.Content); - var templateFile = FileHelper.ParseFileFullPath(template.Content, torrent.SettingPath); - if (File.Exists(templateFile)) - { - Log.Debug("找到了{Site} bbcode文件 {File}", site, templateFile); - template.Content = File.ReadAllText(templateFile); - } - else - { - Log.Error("发布模板看起来是个.bbcode文件,但是这个.bbcode文件不存在{NewLine}" + - "{Source}-->{Dest}", Environment.NewLine, template.Content, templateFile); - return false; - } - } - return true; + return ValidTemplate(template, site, torrent.SettingPath); } } } diff --git a/OKP.Core/Interface/AdapterBase.cs b/OKP.Core/Interface/AdapterBase.cs index f02c583..d872e38 100644 --- a/OKP.Core/Interface/AdapterBase.cs +++ b/OKP.Core/Interface/AdapterBase.cs @@ -1,9 +1,131 @@ -namespace OKP.Core.Interface +using Serilog; +using Markdig; +using Converter.MarkdownToBBCode.Shared; +using Markdig.Extensions.EmphasisExtras; +using Markdig.Parsers; +using OKP.Core.Utils; + +namespace OKP.Core.Interface { internal abstract class AdapterBase { public abstract Task PingAsync(); public abstract Task PostAsync(); + + internal static string ConvertMarkdownToHtml(string mdContent) + { + return Markdown.ToHtml(mdContent); + } + + internal static string ConvertMarkdownToBBCode(string mdContent) + { + var pipeline = new MarkdownPipelineBuilder().EnableTrackTrivia().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough).Build(); + var document = MarkdownParser.Parse(mdContent, pipeline); + + using var sw = new StringWriter(); + var renderer = new BBCodeRenderer(BBCodeType.NexusMods, pipeline, false, false, sw); + renderer.Render(document); + renderer.Writer.Flush(); + + return renderer.Writer.ToString() ?? string.Empty; + } + + private enum ContentType + { + Text, + MarkdownFile, + HtmlFile, + BBCodeFile, + } + + private static string GetContentExt(ContentType contentType) + { + return contentType switch + { + ContentType.MarkdownFile => ".md", + ContentType.HtmlFile => ".html", + ContentType.BBCodeFile => ".bbcode", + _ => throw new ArgumentOutOfRangeException(nameof(contentType), contentType, null) + }; + } + + private static ContentType GetContentType(string site) + { + return site switch + { + "dmhy" => ContentType.HtmlFile, + "acgnx_asia" => ContentType.HtmlFile, + "acgnx_global" => ContentType.HtmlFile, + "acgrip" => ContentType.BBCodeFile, + "bangumi" => ContentType.HtmlFile, + "nyaa" => ContentType.MarkdownFile, + _ => throw new ArgumentOutOfRangeException(nameof(site), site, null) + }; + } + + internal static bool ValidTemplate(TorrentContent.Template template, string site, string? settingPath) + { + if (template.Content == null) return false; + var contentType = ContentType.Text; + + var span = template.Content.AsSpan(); + if (span.IndexOf('\n') != -1) return true; + + if (span.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) + { + contentType = ContentType.MarkdownFile; + } + else if (span.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) + { + contentType = ContentType.HtmlFile; + } + else if (span.EndsWith(".bbcode", StringComparison.OrdinalIgnoreCase)) + { + contentType = ContentType.BBCodeFile; + } + else + { + return true; + } + + var siteContentType = GetContentType(site); + var siteContentExt = GetContentExt(siteContentType); + var contentExt = GetContentExt(contentType); + if (contentType != siteContentType) + { + Log.Debug("不存在{Site} {FileExt}文件,将用 {File} 生成", site, siteContentExt, template.Content); + } + else + { + Log.Debug("开始寻找{Site} {FileExt}文件 {File}", site, siteContentExt, template.Content); + } + var templateFile = FileHelper.ParseFileFullPath(template.Content, settingPath); + if (File.Exists(templateFile)) + { + Log.Debug("找到了{FileExt}文件 {File}", contentExt, templateFile); + var text = File.ReadAllText(templateFile); + if (contentType == siteContentType) + { + template.Content = text; + } + else + { + template.Content = siteContentType switch + { + ContentType.HtmlFile => ConvertMarkdownToHtml(text), + ContentType.BBCodeFile => ConvertMarkdownToBBCode(text), + _ => throw new ArgumentOutOfRangeException() + }; + } + } + else + { + Log.Error("发布模板不存在{NewLine}{Source}-->{Dest}", Environment.NewLine, template.Content, templateFile); + return false; + } + + return true; + } } public class HttpResult { diff --git a/OKP.Core/Interface/Bangumi/BangumiAdapter.cs b/OKP.Core/Interface/Bangumi/BangumiAdapter.cs index 6a7c595..dc41955 100644 --- a/OKP.Core/Interface/Bangumi/BangumiAdapter.cs +++ b/OKP.Core/Interface/Bangumi/BangumiAdapter.cs @@ -155,23 +155,7 @@ private bool Valid() Log.Fatal("{Site} torrent.Data?.TorrentObject is null", site); throw new ArgumentNullException(nameof(torrent.Data.TorrentObject)); } - if (template.Content != null && template.Content.ToLower().EndsWith(".html")) - { - Log.Debug("开始寻找{Site} html文件 {File}", site, template.Content); - var templateFile = FileHelper.ParseFileFullPath(template.Content, torrent.SettingPath); - if (File.Exists(templateFile)) - { - Log.Debug("找到了{Site} html文件 {File}", site, templateFile); - template.Content = File.ReadAllText(templateFile); - } - else - { - Log.Error("发布模板看起来是个.html文件,但是这个.html文件不存在{NewLine}" + - "{Source}-->{Dest}", template.Content, Environment.NewLine, templateFile); - return false; - } - } - return true; + return ValidTemplate(template, site, torrent.SettingPath); } private static List CastTags(List? tags) { diff --git a/OKP.Core/Interface/Dmhy/DmhyAdapter.cs b/OKP.Core/Interface/Dmhy/DmhyAdapter.cs index 28fb30e..934b3d1 100644 --- a/OKP.Core/Interface/Dmhy/DmhyAdapter.cs +++ b/OKP.Core/Interface/Dmhy/DmhyAdapter.cs @@ -148,23 +148,8 @@ private bool Valid() Log.Fatal("{Site} torrent.Data?.TorrentObject is null", site); throw new ArgumentNullException(nameof(torrent.Data.TorrentObject)); } - if (template.Content != null && template.Content.ToLower().EndsWith(".html")) - { - Log.Debug("开始寻找{Site} html文件 {File}", site, template.Content); - var templateFile = FileHelper.ParseFileFullPath(template.Content, torrent.SettingPath); - if (File.Exists(templateFile)) - { - Log.Debug("找到了{Site} html文件 {File}", site, templateFile); - template.Content = File.ReadAllText(templateFile); - } - else - { - Log.Error("发布模板看起来是个.html文件,但是这个.html文件不存在{NewLine}" + - "{Source}-->{Dest}", template.Content, Environment.NewLine, templateFile); - return false; - } - } - return true; + + return ValidTemplate(template, site, torrent.SettingPath); } } } diff --git a/OKP.Core/Interface/Nyaa/NyaaAdapter.cs b/OKP.Core/Interface/Nyaa/NyaaAdapter.cs index f10a90f..731bb71 100644 --- a/OKP.Core/Interface/Nyaa/NyaaAdapter.cs +++ b/OKP.Core/Interface/Nyaa/NyaaAdapter.cs @@ -147,23 +147,7 @@ private bool Valid() return false; } } - if (template.Content != null && template.Content.ToLower().EndsWith(".md")) - { - Log.Debug("开始寻找{Site} .md文件 {File}", site, template.Content); - var templateFile = FileHelper.ParseFileFullPath(template.Content, torrent.SettingPath); - if (File.Exists(templateFile)) - { - Log.Debug("找到了{Site} .md文件 {File}", site, template.Content); - template.Content = File.ReadAllText(templateFile); - } - else - { - Log.Error("发布模板看起来是个.md文件,但是这个.md文件不存在{NewLine}" + - "{Source}-->{Dest}", Environment.NewLine, template.Content, templateFile); - return false; - } - } - return true; + return ValidTemplate(template, site, torrent.SettingPath); } private NyaaTorrentFlags SetFlags(List? flags, List? tags) diff --git a/OKP.Core/Interface/TorrentContent.cs b/OKP.Core/Interface/TorrentContent.cs index 89e325d..685368d 100644 --- a/OKP.Core/Interface/TorrentContent.cs +++ b/OKP.Core/Interface/TorrentContent.cs @@ -94,7 +94,7 @@ public enum ContentTypes App, Game } - public static TorrentContent Build(string filename, string settingFile, string appLocation) + public static TorrentContent Build(string filename, string settingFile, string? baseTemplate, string appLocation) { var settingFilePath = settingFile; @@ -116,6 +116,7 @@ public static TorrentContent Build(string filename, string settingFile, string a } var torrentC = TomlParseHelper.DeserializeTorrentContent(settingFilePath); + ProcessTemplate(torrentC, baseTemplate); torrentC.SettingPath = Path.GetDirectoryName(settingFilePath); //if (!File.Exists(torrentC.CookiePath)) //{ @@ -194,6 +195,24 @@ public static TorrentContent Build(string filename, string settingFile, string a return torrentC; } + + private static void ProcessTemplate(TorrentContent torrentC, string? baseTemplate) + { + if (baseTemplate is null) return; + if (torrentC.IntroTemplate is null) return; + + if (Path.GetDirectoryName(baseTemplate) == "") + { + baseTemplate = Path.Combine(Environment.CurrentDirectory, baseTemplate); + } + + // Automatically generate for sites with missing publishing content + foreach (var site in torrentC.IntroTemplate.Where(site => string.IsNullOrEmpty(site.Content))) + { + site.Content = baseTemplate; + } + } + public bool IsV2() { if (Data?.TorrentObject is null) diff --git a/OKP.Core/OKP.Core.csproj b/OKP.Core/OKP.Core.csproj index e280e2b..874ec2c 100644 --- a/OKP.Core/OKP.Core.csproj +++ b/OKP.Core/OKP.Core.csproj @@ -15,6 +15,7 @@ Released under the GNU GPLv3+. + diff --git a/OKP.Core/Program.cs b/OKP.Core/Program.cs index 42dccc6..97994c1 100644 --- a/OKP.Core/Program.cs +++ b/OKP.Core/Program.cs @@ -56,6 +56,13 @@ public static void Main(string[] args) Description = "Ignore login fail and continue publishing." }; + var baseTemplateOption = new CliOption("--base_template", "-b") + { + DefaultValueFactory = _ => null, + Description = + "Base template. It needs to be a markdown file, and other site templates will be generated based on it which missing publishing content." + }; + var rootCommand = new CliRootCommand("One Key Publish") { torrentArgument, @@ -65,6 +72,7 @@ public static void Main(string[] args) logFileOption, noReactionOption, allowSkipOption, + baseTemplateOption }; rootCommand.SetAction((result, _) => @@ -76,8 +84,9 @@ public static void Main(string[] args) var logFile = result.GetValue(logFileOption); var noReaction = result.GetValue(noReactionOption); var allowSkip = result.GetValue(allowSkipOption); + var baseTemplate = result.GetValue(baseTemplateOption); - ActionHandler(torrentFile, cookies, settingFile!, logLevel!, logFile!, noReaction, allowSkip); + ActionHandler(torrentFile, cookies, settingFile!, logLevel!, logFile!, noReaction, allowSkip, baseTemplate); return Task.CompletedTask; }); @@ -85,7 +94,7 @@ public static void Main(string[] args) IOHelper.ReadLine(); } - private static void ActionHandler(IEnumerable? torrentFile, string? cookies, string settingFile, string logLevel, string logFile, bool noReaction, bool allowSkip) + private static void ActionHandler(IEnumerable? torrentFile, string? cookies, string settingFile, string logLevel, string logFile, bool noReaction, bool allowSkip, string? baseTemplate) { var levelSwitch = new LoggingLevelSwitch { @@ -110,6 +119,13 @@ private static void ActionHandler(IEnumerable? torrentFile, string? cook Log.Fatal("o.TorrentFile is null"); return; } + + if (!string.IsNullOrEmpty(baseTemplate) && Path.GetExtension(baseTemplate) != ".md") + { + Log.Fatal("base_template must be a .md file if specified"); + return; + } + var addCookieCount = 0; foreach (var file in torrentFile) { @@ -123,7 +139,7 @@ private static void ActionHandler(IEnumerable? torrentFile, string? cook if (extension.Equals(".torrent", StringComparison.OrdinalIgnoreCase)) { Log.Information("正在发布 {File}", file); - SinglePublish(file, settingFile, cookies, allowSkip); + SinglePublish(file, settingFile, cookies, allowSkip, baseTemplate); continue; } if (extension.Equals(".txt", StringComparison.OrdinalIgnoreCase)) @@ -190,7 +206,7 @@ private static void ActionHandler(IEnumerable? torrentFile, string? cook } } - private static void SinglePublish(string file, string settingFile, string? cookies,bool allowSkip) + private static void SinglePublish(string file, string settingFile, string? cookies, bool allowSkip, string? baseTemplate) { if (!File.Exists(file)) { @@ -198,7 +214,7 @@ private static void SinglePublish(string file, string settingFile, string? cooki IOHelper.ReadLine(); return; } - var torrent = TorrentContent.Build(file, settingFile, AppDomain.CurrentDomain.BaseDirectory); + var torrent = TorrentContent.Build(file, settingFile, baseTemplate, AppDomain.CurrentDomain.BaseDirectory); if (cookies is null) { if (torrent.CookiePath is not null) diff --git a/README.md b/README.md index 884ff77..56cfcc2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ One-Key-Publish,一键发布 Torrent 到常见 BT 站。 ## Quick Start -依赖:[.NET 6 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) +依赖:[.NET 8 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 1. 导出并添加 Cookie [参考这里](#导入-Cookie-到-OKP),默认的 cookie 文件将保存在程序目录的 `config\cookies` 目录下。 2. 编写一个配置文件 [示例](https://github.com/AmusementClub/OKP/blob/master/OKP.Core/example/setting.toml),将配置文件置于种子文件同目录下。 @@ -17,21 +17,25 @@ One-Key-Publish,一键发布 Torrent 到常见 BT 站。 `OKP.Core yourTorrent.torrent {-s yourSetting.toml} {--cookies yourCookieFile.txt}` ``` - --cookies (Default: cookies.txt) (Not required) Specific Cookie file. - - -s, --setting (Default: setting.toml) (Not required) Specific setting file. - - -l, --log_level (Default: Debug) Log level. - - --log_file (Default: log.txt) Log file. - - -y Skip reaction. - - --help Display this help screen. - - --version Display version information. - - torrent (pos. 0) Required. Torrents to be published.(Or Cookie file exported by Get Cookies.txt.) +Description: + One Key Publish + +Usage: + okp [...] [options] + +Arguments: + Torrents to be published. (Or Cookie file exported by Get Cookies.txt.) + +Options: + -?, -h, --help Show help and usage information + --version Show version information + --cookies Cookie file to be used. [] + -s, --setting (Not required) Specific setting file. [default: setting.toml] + -l, --log_level Log level. [default: Debug] + --log_file Log file. [default: log.txt] + -y, --no_reaction Skip reaction. + --allow_skip Ignore login fail and continue publishing. + -b, --base_template Base template. It needs to be a markdown file, and other site templates will be generated based on it which missing publishing content. [] ``` ### 必选项 @@ -53,6 +57,8 @@ One-Key-Publish,一键发布 Torrent 到常见 BT 站。 - `-l, --log_level`,指定输出的 log level,默认是 `Debug`,可以指定 `Verbose, Debug, Info`(不区分大小写) - `--log_file`,指定输出的 log 文件,默认命名为 `log{当前时间的4位年份}{当前时间的2位月份}.txt`,默认输出位置为执行目录 - `-y`,跳过所有需要回车的地方 +- `--allow_skip`,如果有站点登录失败则自动跳过 +- `-b, --base_template`,在没有填写发布内容时,可以指定一个 markdown 文件,发布内容由这个 markdown 进行转换生成 ## 配置文件 @@ -90,6 +96,7 @@ One-Key-Publish,一键发布 Torrent 到常见 BT 站。 3. `proxy`:连接站点使用的代理 4. `content`:发布正文,可以指定一个文件名或 raw string - 指定文件名的后缀同见 #支持站点 + - 如果配合 `-b, --base_template` 使用可以不用填写 ### publish template