Compare commits
37 Commits
81e04c90a3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b304269079 | ||
|
|
e7168a2fe6 | ||
|
|
626a8a2950 | ||
|
|
a650d4197a | ||
|
|
f93cd5b405 | ||
|
|
e682814630 | ||
|
|
33b17c9937 | ||
|
|
e739638fae | ||
|
|
7e67e26bb2 | ||
| 46832d78a2 | |||
|
|
2c728891a5 | ||
|
|
a60c058a49 | ||
|
|
b492658dc6 | ||
|
|
aa1970a36e | ||
|
|
1e4da61d68 | ||
|
|
90ccaba1a3 | ||
|
|
cb41791656 | ||
|
|
8ebab0d6f8 | ||
|
|
7a18330524 | ||
|
|
84e35fb631 | ||
|
|
6c443f2fde | ||
|
|
3403881b2a | ||
|
|
2364e41825 | ||
|
|
d379f516c5 | ||
|
|
694db295a9 | ||
|
|
8ceb88846c | ||
|
|
84357b7f31 | ||
|
|
6773efb842 | ||
|
|
25ace6703f | ||
|
|
903fd8e256 | ||
|
|
3288df0f25 | ||
|
|
ec3434da82 | ||
|
|
bf4faca1a7 | ||
|
|
fae4754df4 | ||
|
|
7b0c5f8ce2 | ||
|
|
4dbdf69c25 | ||
|
|
9981dfa211 |
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// 使用 IntelliSense 找出 C# 调试存在哪些属性
|
||||
// 将悬停用于现有属性的说明
|
||||
// 有关详细信息,请访问 https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md。
|
||||
"name": ".NET Core Launch (web)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// 如果已更改目标框架,请确保更新程序路径。
|
||||
"program": "${workspaceFolder}/src/WebApi/bin/Debug/net8.0/WebApi.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/WebApi",
|
||||
"stopAtEntry": false,
|
||||
// 启用在启动 ASP.NET Core 时启动 Web 浏览器。有关详细信息: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/NasRobot.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/NasRobot.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/NasRobot.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,16 +6,17 @@
|
||||
container_name: nas_robot
|
||||
restart: always
|
||||
privileged: true
|
||||
user: root
|
||||
user: "root:root"
|
||||
networks:
|
||||
- all_in
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /wd/volc/media/tv:/data/media/tv:rw
|
||||
- /wd/volc/media/anime:/data/media/anime:rw
|
||||
- /wd/volc/media/anime-other:/data/media/anime-other:rw
|
||||
- /wd/media/tv:/data/media/tv:rw
|
||||
- /wd/media/anime:/data/media/anime:rw
|
||||
- /wd/media/music:/data/media/music:rw
|
||||
- /wd/media/others/anime:/data/media/anime-other:rw
|
||||
- /wd/apps/vols/nas_robot:/app/data:rw
|
||||
- /wd:/host/wd:ro
|
||||
- /proc/mounts:/host/proc/mounts:ro
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Core;
|
||||
|
||||
public static class Command
|
||||
{
|
||||
public static async Task<(HttpStatusCode responseCode, string responseStr)> ExecAsync(params string[] commands)
|
||||
{
|
||||
var execUrl = "http://192.168.31.14:45321/exec";
|
||||
|
||||
var client = new HttpClient();
|
||||
var content = JsonContent.Create(new
|
||||
{
|
||||
Token = "4CeVaIQGbB4Qln9%V@Bh8bMYSpHIUV66",
|
||||
Script = commands
|
||||
});
|
||||
|
||||
var response = await client.PostAsync(execUrl, content);
|
||||
|
||||
return (response.StatusCode, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Core;
|
||||
|
||||
public static class Tools
|
||||
{
|
||||
public static long ToUnixTimeMilliseconds(this DateTime dateTime)
|
||||
{
|
||||
return new DateTimeOffset(dateTime).ToUnixTimeMilliseconds();
|
||||
}
|
||||
}
|
||||
@@ -4,34 +4,6 @@ namespace Core;
|
||||
|
||||
public static class WxNotify
|
||||
{
|
||||
public static async Task SendDiskInfoAsync(string msg)
|
||||
{
|
||||
// 晚上11点到早上8点不通知,输出到控制台
|
||||
if (DateTime.Now.Hour >= 23 || DateTime.Now.Hour < 8)
|
||||
{
|
||||
Console.WriteLine("======================Skip WxNotify======================");
|
||||
Console.WriteLine(msg);
|
||||
Console.WriteLine("======================Skip WxNotify======================");
|
||||
return;
|
||||
}
|
||||
|
||||
var wxRebotUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=96f9fa23-a959-4492-ac3a-7415fae19680";
|
||||
var client = new HttpClient();
|
||||
var requestBody =
|
||||
"""
|
||||
{
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": "{Msg}"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
// 转义
|
||||
requestBody = requestBody.Replace("{Msg}", msg.Replace("\r\n", "\n"));
|
||||
await client.PostAsync(wxRebotUrl, new StringContent(requestBody, Encoding.UTF8, "application/json"));
|
||||
}
|
||||
|
||||
public static async Task SendCommonAsync(string msg)
|
||||
{
|
||||
var wxRebotUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=96f9fa23-a959-4492-ac3a-7415fae19680";
|
||||
|
||||
@@ -2,5 +2,11 @@ namespace Interface.Jobs;
|
||||
|
||||
public interface IChineseNfoRegistry
|
||||
{
|
||||
void Job(bool ignoreLocked, bool ignoreCompleted);
|
||||
void Job(
|
||||
string? path = null,
|
||||
int? seasonNumber = null,
|
||||
int? episodeNumber = null,
|
||||
bool ignoreLocked = false,
|
||||
bool ignoreCompleted = false
|
||||
);
|
||||
}
|
||||
@@ -1,482 +0,0 @@
|
||||
// using System.Diagnostics;
|
||||
// using System.Net;
|
||||
// using System.Text.Json.Nodes;
|
||||
// using System.Xml;
|
||||
// using Core;
|
||||
// using FluentScheduler;
|
||||
// using Interface.Jobs;
|
||||
// using Microsoft.Extensions.Configuration;
|
||||
// using Microsoft.Extensions.Logging;
|
||||
//
|
||||
// namespace Service.Jobs;
|
||||
//
|
||||
// public class ChineseNfoRegistry : Registry, IChineseNfoRegistry
|
||||
// {
|
||||
// private readonly IConfiguration _configuration;
|
||||
// private readonly ILogger<ChineseNfoRegistry> _logger;
|
||||
// private readonly HttpClient _client;
|
||||
//
|
||||
// public ChineseNfoRegistry(IConfiguration configuration, ILogger<ChineseNfoRegistry> logger)
|
||||
// {
|
||||
// _configuration = configuration;
|
||||
// _logger = logger;
|
||||
//
|
||||
// var httpClientHandler = new HttpClientHandler();
|
||||
//
|
||||
// var proxyAddress = _configuration["ChineseNfo:HttpProxy"];
|
||||
//
|
||||
// if (string.IsNullOrEmpty(proxyAddress) == false)
|
||||
// {
|
||||
// httpClientHandler.Proxy = string.IsNullOrEmpty(proxyAddress) ? null : new WebProxy(proxyAddress, false);
|
||||
// httpClientHandler.UseProxy = !string.IsNullOrEmpty(proxyAddress);
|
||||
// }
|
||||
//
|
||||
// _client = new HttpClient(httpClientHandler);
|
||||
// _client.BaseAddress = new Uri("http://api.themoviedb.org");
|
||||
//
|
||||
// Schedule(() => Job(true, true)).ToRunEvery(1).Days();
|
||||
// }
|
||||
//
|
||||
// public async void Job(bool ignoreLocked, bool ignoreCompleted)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// await JobExecute(ignoreLocked, ignoreCompleted);
|
||||
// }
|
||||
// catch (Exception e)
|
||||
// {
|
||||
// _logger.LogError(e, "ChineseNfoRegistry.Job() error");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private async Task JobExecute(bool ignoreLocked, bool ignoreCompleted)
|
||||
// {
|
||||
// // 读取环境变量 tv folder
|
||||
// var tvFolder = _configuration["ChineseNfo:TvFolder"];
|
||||
// if (string.IsNullOrEmpty(tvFolder))
|
||||
// {
|
||||
// Console.WriteLine("请设置环境变量 tv_folder");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// var successCount = 0;
|
||||
// var failedCount = 0;
|
||||
// var skippedCount = 0;
|
||||
//
|
||||
// // 扫描 tvshow.nfo 文件
|
||||
// var tvShowFiles = Directory.GetFiles(tvFolder, "tvshow.nfo", SearchOption.AllDirectories);
|
||||
//
|
||||
// foreach (var tvShowFile in tvShowFiles)
|
||||
// {
|
||||
// var nfoContent = File.ReadAllText(tvShowFile);
|
||||
//
|
||||
// // 读取 使用XML解析器 读取 <uniqueid type="tmdb">60059</uniqueid>
|
||||
// var tvXml = new XmlDocument();
|
||||
// tvXml.LoadXml(nfoContent);
|
||||
// var uniqueIdNode = tvXml.SelectSingleNode("//uniqueid[@type='tmdb']");
|
||||
// if (uniqueIdNode == null)
|
||||
// {
|
||||
// Console.WriteLine($"{tvShowFile} & 未找到 tmdb id");
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// if (!int.TryParse(uniqueIdNode.InnerText, out var tmdbId))
|
||||
// {
|
||||
// Console.WriteLine($"{tvShowFile} & tmdb id 不是数字");
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// await SetTvInfo(tvShowFile, tvXml, tmdbId);
|
||||
//
|
||||
// // 获取 tvShowFile 的文件夹
|
||||
// var folderPath = Path.GetDirectoryName(tvShowFile);
|
||||
//
|
||||
// if (folderPath == null)
|
||||
// {
|
||||
// Console.WriteLine($"{tvShowFile} & 未找到文件夹");
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// // 扫描文件夹下其他 nfo 文件
|
||||
// var seasonFiles = Directory.GetFiles(folderPath, "*.nfo", SearchOption.AllDirectories);
|
||||
//
|
||||
// seasonFiles = seasonFiles.Where(x => !x.EndsWith("tvshow.nfo")).ToArray();
|
||||
//
|
||||
// foreach (var episodeFile in seasonFiles)
|
||||
// {
|
||||
// await SetEpisodeInfo(episodeFile, tmdbId);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// await WxNotify.SendCommonAsync($"ChineseNfoRegistry.Job() success: {successCount}, failed: {failedCount}, skipped: {skippedCount}");
|
||||
//
|
||||
// async Task SetTvInfo(string tvShowFile,XmlDocument tvXml, int tmdbId)
|
||||
// {
|
||||
// // 检查 lockdata
|
||||
// var lockedNode = tvXml.SelectSingleNode("//lockdata");
|
||||
// if (lockedNode != null && lockedNode.InnerText == "true" && ignoreLocked == false)
|
||||
// {
|
||||
// skippedCount++;
|
||||
// Console.WriteLine($"{tvShowFile} & 已锁定");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 获取tv info
|
||||
// var tvJson = await GetTmdbTv(tvShowFile, tmdbId);
|
||||
//
|
||||
// if (tvJson == null)
|
||||
// {
|
||||
// failedCount++;
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// var title = tvJson["name"]?.ToString();
|
||||
// var overview = tvJson["overview"]?.ToString();
|
||||
// var poster =tvJson["poster_path"]?.ToString();
|
||||
// var genres = tvJson["genres"]
|
||||
// ?.AsArray()
|
||||
// .Select(x => x?["name"]?.ToString())
|
||||
// .Where(x=> !string.IsNullOrEmpty(x))
|
||||
// .ToList();
|
||||
//
|
||||
// if (!string.IsNullOrEmpty(title))
|
||||
// {
|
||||
// title = $"<![CDATA[{title}]]>";
|
||||
// var titleNode = tvXml.SelectSingleNode("//title");
|
||||
// if (titleNode != null)
|
||||
// {
|
||||
// if (titleNode.InnerXml != title)
|
||||
// {
|
||||
// titleNode.InnerXml = title;
|
||||
// successCount++;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// var sorttitleNode = tvXml.SelectSingleNode("//sorttitle");
|
||||
// if (sorttitleNode != null)
|
||||
// {
|
||||
// if (sorttitleNode.InnerXml != title)
|
||||
// {
|
||||
// sorttitleNode.InnerXml = title;
|
||||
// successCount++;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (!string.IsNullOrEmpty(overview))
|
||||
// {
|
||||
// overview = $"<![CDATA[{overview}]]>";
|
||||
// var plotNode = tvXml.SelectSingleNode("//plot");
|
||||
// if (plotNode != null)
|
||||
// {
|
||||
// if (plotNode.InnerXml != overview)
|
||||
// {
|
||||
// plotNode.InnerXml = overview;
|
||||
// successCount++;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// var outlineNode = tvXml.SelectSingleNode("//outline");
|
||||
// if (outlineNode != null)
|
||||
// {
|
||||
// if (outlineNode.InnerXml != overview)
|
||||
// {
|
||||
// outlineNode.InnerXml = overview;
|
||||
// successCount++;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if(!string.IsNullOrEmpty(poster))
|
||||
// {
|
||||
// poster = $"http://image.tmdb.org/t/p/w1280/{poster}";
|
||||
// // 下载并覆盖海报
|
||||
// var posterPath = tvShowFile.Replace("tvshow.nfo", $"poster.{poster.Split('.').Last()}");
|
||||
//
|
||||
// using var client = new HttpClient();
|
||||
// var response = await client.GetAsync(poster);
|
||||
// if (response.IsSuccessStatusCode)
|
||||
// {
|
||||
// var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
//
|
||||
// if (File.Exists(posterPath))
|
||||
// {
|
||||
// File.Delete(posterPath);
|
||||
// }
|
||||
//
|
||||
// await File.WriteAllBytesAsync(posterPath, bytes);
|
||||
// successCount++;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (genres?.Any() == true)
|
||||
// {
|
||||
// // 删除原有的 <genre> 节点
|
||||
// var genreNodes = tvXml.SelectNodes("//genre");
|
||||
// if (genreNodes != null)
|
||||
// {
|
||||
// foreach (XmlNode genreNode in genreNodes)
|
||||
// {
|
||||
// tvXml.DocumentElement?.RemoveChild(genreNode);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 添加新的 <genre> 节点
|
||||
// foreach (var genre in genres)
|
||||
// {
|
||||
// var genreNode = tvXml.CreateElement("genre");
|
||||
// genreNode.InnerText = genre!;
|
||||
// tvXml.DocumentElement?.AppendChild(genreNode);
|
||||
// }
|
||||
// successCount++;
|
||||
// }
|
||||
//
|
||||
// if (successCount == 0)
|
||||
// {
|
||||
// skippedCount++;
|
||||
// Console.WriteLine($"{tvShowFile} & 无更新");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 锁定节点
|
||||
// if(lockedNode == null)
|
||||
// {
|
||||
// lockedNode = tvXml.CreateElement("lockdata");
|
||||
// tvXml.DocumentElement?.AppendChild(lockedNode);
|
||||
// lockedNode.InnerText = "true";
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// lockedNode.InnerText = "true";
|
||||
// }
|
||||
//
|
||||
// // 添加一个已完成节点
|
||||
// var completedNode = tvXml.SelectSingleNode("//completed");
|
||||
// if (completedNode == null)
|
||||
// {
|
||||
// completedNode = tvXml.CreateElement("completed");
|
||||
// tvXml.DocumentElement?.AppendChild(completedNode);
|
||||
// }
|
||||
// completedNode.InnerText = "true";
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// tvXml.Save(tvShowFile);
|
||||
// }
|
||||
// catch (Exception e)
|
||||
// {
|
||||
// Console.WriteLine(e);
|
||||
// failedCount++;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// async Task SetEpisodeInfo(string episodeFile, int tmdbId)
|
||||
// {
|
||||
// var episodeContent = await File.ReadAllTextAsync(episodeFile);
|
||||
// var episodeXml = new XmlDocument();
|
||||
// episodeXml.LoadXml(episodeContent);
|
||||
//
|
||||
// // 判断有无 completed 节点
|
||||
// var completedNode = episodeXml.SelectSingleNode("//completed");
|
||||
// if (completedNode != null && ignoreCompleted == false)
|
||||
// {
|
||||
// skippedCount++;
|
||||
// Console.WriteLine($"{episodeFile} & 已完成");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 判断 locked == true
|
||||
// var lockedNode = episodeXml.SelectSingleNode("//lockdata");
|
||||
// if (lockedNode != null && lockedNode.InnerText == "true" && ignoreLocked == false)
|
||||
// {
|
||||
// skippedCount++;
|
||||
// Console.WriteLine($"{episodeFile} & 已锁定");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 读取 <season>1</season>
|
||||
// var seasonNode = episodeXml.SelectSingleNode("//season");
|
||||
// if (seasonNode == null)
|
||||
// {
|
||||
// failedCount++;
|
||||
// Console.WriteLine($"{episodeFile} & 未找到 season");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 读取 <episode>1</episode>
|
||||
// var episodeNode = episodeXml.SelectSingleNode("//episode");
|
||||
// if (episodeNode == null)
|
||||
// {
|
||||
// failedCount++;
|
||||
// Console.WriteLine($"{episodeFile} & 未找到 episode");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// if (!int.TryParse(seasonNode.InnerText, out var season))
|
||||
// {
|
||||
// failedCount++;
|
||||
// Console.WriteLine($"{episodeFile} & season 不是数字");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// if (!int.TryParse(episodeNode.InnerText, out var episode))
|
||||
// {
|
||||
// failedCount++;
|
||||
// Console.WriteLine($"{episodeFile} & episode 不是数字");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 设置 title
|
||||
// var titleNode = episodeXml.SelectSingleNode("//title");
|
||||
// if (titleNode == null)
|
||||
// {
|
||||
// failedCount++;
|
||||
// Console.WriteLine($"{episodeFile} & 未找到 title");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// var json = await GetTmdbEpisode(
|
||||
// episodeFile,
|
||||
// tmdbId,
|
||||
// season,
|
||||
// episode);
|
||||
//
|
||||
// if (json == null)
|
||||
// {
|
||||
// failedCount++;
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// var title = json["name"]?.ToString();
|
||||
// var overview = json["overview"]?.ToString();
|
||||
//
|
||||
// var isUpdate = false;
|
||||
// if (!string.IsNullOrEmpty(title))
|
||||
// {
|
||||
// title = $"<![CDATA[{title}]]>";
|
||||
//
|
||||
// if (titleNode.InnerXml != title)
|
||||
// {
|
||||
// isUpdate = true;
|
||||
// // 写入且不转义
|
||||
// titleNode.InnerXml = title;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// var plotNode = episodeXml.SelectSingleNode("//plot");
|
||||
//
|
||||
// if (!string.IsNullOrEmpty(overview) && plotNode != null)
|
||||
// {
|
||||
// overview = $"<![CDATA[{overview}]]>";
|
||||
// if (plotNode.InnerXml != overview)
|
||||
// {
|
||||
// isUpdate = true;
|
||||
// plotNode.InnerXml = overview;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (!isUpdate)
|
||||
// {
|
||||
// skippedCount++;
|
||||
// Console.WriteLine($"{episodeFile} & 无更新");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// successCount++;
|
||||
//
|
||||
// // 添加一个已完成节点
|
||||
// if (completedNode == null)
|
||||
// {
|
||||
// episodeXml.DocumentElement?.AppendChild(episodeXml.CreateElement("completed"));
|
||||
// }
|
||||
//
|
||||
// // 锁定
|
||||
// if (lockedNode == null)
|
||||
// {
|
||||
// lockedNode = episodeXml.CreateElement("lockdata");
|
||||
// episodeXml.DocumentElement?.AppendChild(lockedNode);
|
||||
// lockedNode.InnerText = "true";
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// lockedNode.InnerText = "true";
|
||||
// }
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// episodeXml.Save(episodeFile);
|
||||
// }
|
||||
// catch (Exception e)
|
||||
// {
|
||||
// Console.WriteLine(e);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// Console.WriteLine($"{episodeFile} & {title} & {overview}");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private async Task<JsonObject?> GetTmdbTv(
|
||||
// string path,
|
||||
// int tmdbId)
|
||||
// {
|
||||
// const string tvUrl = "/3/tv/{0}?api_key=e28e1bc408db7adefc8bacce225c5085&language=zh-CN";
|
||||
// var requestUrl = string.Format(tvUrl, tmdbId);
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// var response = await _client.GetAsync(requestUrl);
|
||||
//
|
||||
// if (!response.IsSuccessStatusCode)
|
||||
// {
|
||||
// Console.WriteLine($"{requestUrl} & {path} & {response.StatusCode}");
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// await Task.Delay(10000);
|
||||
//
|
||||
// var content = await response.Content.ReadAsStringAsync();
|
||||
// var json = JsonNode.Parse(content);
|
||||
// return json as JsonObject;
|
||||
// }
|
||||
// catch (Exception e)
|
||||
// {
|
||||
// Console.WriteLine($"{requestUrl} & {path} \r {e}");
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private async Task<JsonObject?> GetTmdbEpisode(
|
||||
// string path,
|
||||
// int tmdbId,
|
||||
// int season,
|
||||
// int episode)
|
||||
// {
|
||||
// const string episodeUrl = "/3/tv/{0}/season/{1}/episode/{2}?api_key=e28e1bc408db7adefc8bacce225c5085&language=zh-CN";
|
||||
// var requestUrl = string.Format(episodeUrl, tmdbId, season, episode);
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// var response = await _client.GetAsync(requestUrl);
|
||||
//
|
||||
// await Task.Delay(10000);
|
||||
//
|
||||
// if (!response.IsSuccessStatusCode)
|
||||
// {
|
||||
// Console.WriteLine($"{requestUrl} & {path} & {response.StatusCode}");
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// var content = await response.Content.ReadAsStringAsync();
|
||||
// var json = JsonNode.Parse(content);
|
||||
// return json as JsonObject;
|
||||
// }
|
||||
// catch (Exception e)
|
||||
// {
|
||||
// Console.WriteLine($"{requestUrl} & {path} \r {e}");
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using Core;
|
||||
using FluentScheduler;
|
||||
using FreeSql;
|
||||
using FreeSql.DataAnnotations;
|
||||
@@ -44,105 +46,575 @@ public class ChineseNfoRegistry : Registry, IChineseNfoRegistry
|
||||
.UseAutoSyncStructure(true)
|
||||
.Build();
|
||||
|
||||
Schedule(() => Job(true, true)).ToRunEvery(1).Days();
|
||||
Schedule(() => Job(ignoreLocked: true, ignoreCompleted: true)).ToRunEvery(1).Days();
|
||||
}
|
||||
|
||||
public async void Job(bool ignoreLocked, bool ignoreCompleted)
|
||||
public void Job(
|
||||
string? path = null,
|
||||
int? seasonNumber = null,
|
||||
int? episodeNumber = null,
|
||||
bool ignoreLocked = false,
|
||||
bool ignoreCompleted = false
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
await JobExecute(ignoreLocked, ignoreCompleted);
|
||||
var lockAll = "lock-all";
|
||||
lock (lockAll)
|
||||
{
|
||||
var lockKey = $"{path}-{seasonNumber}-{episodeNumber}";
|
||||
|
||||
lock (lockKey)
|
||||
{
|
||||
JobExecute(path, seasonNumber, episodeNumber, ignoreLocked, ignoreCompleted).Wait();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "ChineseNfoRegistry.Job() error");
|
||||
_logger.LogError(e, "error");
|
||||
WxNotify.SendCommonAsync($"ChineseNfoRegistry.Job() 执行失败 {e.Message}").Wait();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogInformation("ChineseNfoRegistry.Job() 执行结束");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task JobExecute(
|
||||
bool ignoreLocked,
|
||||
bool ignoreCompleted,
|
||||
string[]? includeFields = null)
|
||||
string? requestPath = null,
|
||||
int? requestSeasonNumber = null,
|
||||
int? requestEpisodeNumber = null,
|
||||
bool ignoreLocked = false,
|
||||
bool ignoreCompleted = false)
|
||||
{
|
||||
var tvFolder = _configuration["ChineseNfo:TvFolder"];
|
||||
|
||||
if (string.IsNullOrEmpty(tvFolder))
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.Job() tvFolder is null or empty");
|
||||
_logger.LogError("tvFolder is null or empty");
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. 支持指定字段
|
||||
* 2. 接口数据保留
|
||||
* 3. 支持语言降级到繁体
|
||||
* 4. 支持演员名称翻译
|
||||
*/
|
||||
|
||||
var fields =
|
||||
includeFields ??
|
||||
[
|
||||
"tv.title",
|
||||
"tv.plot",
|
||||
"tv.outline",
|
||||
"tv.poster",
|
||||
"tv.genre",
|
||||
"tv.actor.name",
|
||||
"tv.actor.role",
|
||||
"season.title",
|
||||
"season.plot",
|
||||
"episode.title",
|
||||
"episode.plot",
|
||||
"episode.actor.name",
|
||||
];
|
||||
|
||||
var tvNfos = Directory.GetFiles(tvFolder, "tvshow.nfo", SearchOption.AllDirectories);
|
||||
|
||||
if (tvNfos.Length == 0)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.Job() tvNfo is null or empty");
|
||||
_logger.LogError("tvNfo is null or empty");
|
||||
return;
|
||||
}
|
||||
|
||||
var ctn = new ChineseNfoContent();
|
||||
|
||||
foreach (var tv in tvNfos)
|
||||
{
|
||||
await HandleTv(tv);
|
||||
ctn.tvNfoPath = tv;
|
||||
|
||||
if (await HandleTv() == false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sseasons = Directory.GetDirectories(Path.GetDirectoryName(tv) ?? string.Empty, "Season *", SearchOption.AllDirectories);
|
||||
var sseasonsNumbers = sseasons.Select(x => x.Split("Season ")[1]).Select(int.Parse).ToList();
|
||||
|
||||
foreach (var seasonNumber in sseasonsNumbers)
|
||||
{
|
||||
ctn.seasonNumber = seasonNumber;
|
||||
ctn.seasonNfoPath = Path.Combine(Path.GetDirectoryName(tv) ?? string.Empty, $"Season {seasonNumber}", "season.nfo");
|
||||
|
||||
if (requestSeasonNumber != null && requestSeasonNumber != seasonNumber)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (File.Exists(ctn.seasonNfoPath) && await HandleSeason() == false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var episodeNfos = Directory
|
||||
.GetFiles(Path.GetDirectoryName(ctn.seasonNfoPath) ?? string.Empty, "*.nfo", SearchOption.AllDirectories)
|
||||
.Where(x => !x.EndsWith("season.nfo"))
|
||||
.ToList();
|
||||
|
||||
foreach (var episode in episodeNfos)
|
||||
{
|
||||
ctn.episodeNfoPath = episode;
|
||||
|
||||
_logger.LogInformation("ctn.episodeNfoPath: " + ctn.episodeNfoPath);
|
||||
|
||||
try
|
||||
{
|
||||
await HandleEpisode();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async Task<string[]?> HandleTv(string tvNfo)
|
||||
async Task<bool> HandleTv()
|
||||
{
|
||||
var nfoContent = File.ReadAllText(tvNfo);
|
||||
if (!string.IsNullOrEmpty(requestPath))
|
||||
{
|
||||
var latestPath = Path.GetFileName(requestPath) ?? string.Empty;
|
||||
|
||||
if (!ctn.tvNfoPath.Contains(latestPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("开始处理 TV");
|
||||
|
||||
var nfoContent = File.ReadAllText(ctn.tvNfoPath);
|
||||
|
||||
var tvXml = new XmlDocument();
|
||||
tvXml.LoadXml(nfoContent);
|
||||
|
||||
var uniqueIdNode = tvXml.SelectSingleNode("//uniqueid[@type='tmdb']");
|
||||
|
||||
if (uniqueIdNode == null)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.Job() uniqueIdNode is null");
|
||||
return null;
|
||||
_logger.LogError("uniqueIdNode is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(uniqueIdNode.InnerText, out var tmdbId))
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.Job() tmdbId is null");
|
||||
_logger.LogError("tmdbId is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
ctn.tvId = tmdbId;
|
||||
|
||||
var isLockedNode = tvXml.SelectSingleNode("//locked");
|
||||
|
||||
if (isLockedNode != null && isLockedNode.InnerText == "true" && ignoreLocked)
|
||||
{
|
||||
_logger.LogInformation("tvNfo is locked");
|
||||
return true;
|
||||
}
|
||||
|
||||
var isCompletedNode = tvXml.SelectSingleNode("//completed");
|
||||
|
||||
if (isCompletedNode != null && isCompletedNode.InnerText == "true" && ignoreCompleted)
|
||||
{
|
||||
_logger.LogInformation("tvNfo is completed");
|
||||
return true;
|
||||
}
|
||||
|
||||
var tvInfo = await GetTmdbTv(ctn.tvNfoPath, tmdbId);
|
||||
|
||||
if (tvInfo == null)
|
||||
{
|
||||
_logger.LogError("tvInfo is null");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tvInfo["name"] != null)
|
||||
{
|
||||
var titleNode = tvXml.SelectSingleNode("//title");
|
||||
if (titleNode != null)
|
||||
{
|
||||
titleNode.InnerXml = $"<![CDATA[{tvInfo["name"]}]]>";
|
||||
}
|
||||
|
||||
var sorttitleNode = tvXml.SelectSingleNode("//sorttitle");
|
||||
if (sorttitleNode != null)
|
||||
{
|
||||
sorttitleNode.InnerXml = $"<![CDATA[{tvInfo["name"]}]]>";
|
||||
}
|
||||
|
||||
_logger.LogInformation("tvInfo: {tvInfo}", tvInfo["name"]);
|
||||
}
|
||||
|
||||
if (tvInfo["overview"] != null)
|
||||
{
|
||||
var plotNode = tvXml.SelectSingleNode("//plot");
|
||||
if (plotNode != null)
|
||||
{
|
||||
plotNode.InnerXml = $"<![CDATA[{tvInfo["overview"]}]]>";
|
||||
}
|
||||
|
||||
var outlineNode = tvXml.SelectSingleNode("//outline");
|
||||
if (outlineNode != null)
|
||||
{
|
||||
outlineNode.InnerXml = $"<![CDATA[{tvInfo["overview"]}]]>";
|
||||
}
|
||||
|
||||
_logger.LogInformation("tvInfo: {tvInfo}", tvInfo["overview"]);
|
||||
}
|
||||
|
||||
if (tvInfo["poster_path"] != null)
|
||||
{
|
||||
var posterPath = tvInfo["poster_path"]!.ToString();
|
||||
var image = await GetTmdbImage(ctn.tvNfoPath, posterPath);
|
||||
if (image != null)
|
||||
{
|
||||
var imagePath = Path.Combine(Path.GetDirectoryName(ctn.tvNfoPath) ?? string.Empty, "poster" + Path.GetExtension(posterPath));
|
||||
await File.WriteAllBytesAsync(imagePath, image);
|
||||
}
|
||||
}
|
||||
|
||||
var actors = tvXml.SelectNodes("//actor");
|
||||
if (actors != null)
|
||||
{
|
||||
foreach (XmlNode actor in actors)
|
||||
{
|
||||
await HandleActor(actor);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("tmdbId: {tmdbId}, name: {name}", tmdbId, tvInfo["name"]?.ToString());
|
||||
|
||||
if (isLockedNode != null)
|
||||
{
|
||||
isLockedNode.InnerText = "true";
|
||||
}
|
||||
else
|
||||
{
|
||||
isLockedNode = tvXml.CreateElement("locked");
|
||||
tvXml.DocumentElement?.AppendChild(isLockedNode);
|
||||
isLockedNode.InnerText = "true";
|
||||
}
|
||||
|
||||
if (isCompletedNode != null)
|
||||
{
|
||||
isCompletedNode.InnerText = "true";
|
||||
}
|
||||
else
|
||||
{
|
||||
isCompletedNode = tvXml.CreateElement("completed");
|
||||
tvXml.DocumentElement?.AppendChild(isCompletedNode);
|
||||
isCompletedNode.InnerText = "true";
|
||||
}
|
||||
|
||||
// 保存
|
||||
tvXml.Save(ctn.tvNfoPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async Task<bool> HandleSeason()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ctn.seasonNfoPath))
|
||||
{
|
||||
_logger.LogError("seasonNfoPath is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
var nfoContent = File.ReadAllText(ctn.seasonNfoPath);
|
||||
|
||||
var seasonXml = new XmlDocument();
|
||||
seasonXml.LoadXml(nfoContent);
|
||||
|
||||
var seasonNumberNode = seasonXml.SelectSingleNode("//seasonnumber");
|
||||
|
||||
if (seasonNumberNode == null)
|
||||
{
|
||||
_logger.LogError("seasonNumberNode is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(seasonNumberNode.InnerText, out var seasonNumber))
|
||||
{
|
||||
_logger.LogError("seasonNumber is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
ctn.seasonNumber = seasonNumber;
|
||||
|
||||
_logger.LogInformation("开始处理 Season");
|
||||
|
||||
var isLockedNode = seasonXml.SelectSingleNode("//locked");
|
||||
|
||||
if (isLockedNode != null && isLockedNode.InnerText == "true" && ignoreLocked)
|
||||
{
|
||||
_logger.LogInformation("seasonNfo is locked");
|
||||
return true;
|
||||
}
|
||||
|
||||
var isCompletedNode = seasonXml.SelectSingleNode("//completed");
|
||||
|
||||
if (isCompletedNode != null && isCompletedNode.InnerText == "true" && ignoreCompleted)
|
||||
{
|
||||
_logger.LogInformation("seasonNfo is completed");
|
||||
return true;
|
||||
}
|
||||
|
||||
var seasonInfo = await GetTmdbSeason(ctn);
|
||||
|
||||
if (seasonInfo == null)
|
||||
{
|
||||
_logger.LogError("seasonInfo is null");
|
||||
return true;
|
||||
}
|
||||
|
||||
var titleNode = seasonXml.SelectSingleNode("//sorttitle");
|
||||
if (titleNode != null && seasonInfo["name"] != null)
|
||||
{
|
||||
_logger.LogInformation("seasonInfo: {seasonInfo}", seasonInfo["name"]);
|
||||
titleNode.InnerXml = $"<![CDATA[{seasonInfo["name"]}]]>";
|
||||
}
|
||||
|
||||
var plotNode = seasonXml.SelectSingleNode("//plot");
|
||||
if (plotNode != null && seasonInfo["overview"] != null)
|
||||
{
|
||||
_logger.LogInformation("seasonInfo: {seasonInfo}", seasonInfo["overview"]);
|
||||
plotNode.InnerXml = $"<![CDATA[{seasonInfo["overview"]}]]>";
|
||||
}
|
||||
|
||||
var outlineNode = seasonXml.SelectSingleNode("//outline");
|
||||
if (outlineNode != null && seasonInfo["overview"] != null)
|
||||
{
|
||||
_logger.LogInformation("seasonInfo: {seasonInfo}", seasonInfo["overview"]);
|
||||
outlineNode.InnerXml = $"<![CDATA[{seasonInfo["overview"]}]]>";
|
||||
}
|
||||
|
||||
var poster_path = seasonInfo["poster_path"]?.ToString();
|
||||
if (poster_path != null)
|
||||
{
|
||||
var image = await GetTmdbImage(ctn.seasonNfoPath, poster_path);
|
||||
if (image != null)
|
||||
{
|
||||
var imagePath = Path.Combine(Path.GetDirectoryName(ctn.tvNfoPath) ?? string.Empty, "season" + seasonNumber.ToString("D2") + "-poster" + Path.GetExtension(poster_path));
|
||||
await File.WriteAllBytesAsync(imagePath, image);
|
||||
}
|
||||
}
|
||||
|
||||
if (isLockedNode != null)
|
||||
{
|
||||
isLockedNode.InnerText = "true";
|
||||
}
|
||||
else
|
||||
{
|
||||
isLockedNode = seasonXml.CreateElement("locked");
|
||||
seasonXml.DocumentElement?.AppendChild(isLockedNode);
|
||||
isLockedNode.InnerText = "true";
|
||||
}
|
||||
|
||||
if (isCompletedNode != null)
|
||||
{
|
||||
isCompletedNode.InnerText = "true";
|
||||
}
|
||||
else
|
||||
{
|
||||
isCompletedNode = seasonXml.CreateElement("completed");
|
||||
seasonXml.DocumentElement?.AppendChild(isCompletedNode);
|
||||
isCompletedNode.InnerText = "true";
|
||||
}
|
||||
|
||||
seasonXml.Save(ctn.seasonNfoPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async Task HandleEpisode()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ctn.episodeNfoPath))
|
||||
{
|
||||
_logger.LogError("episodeNfoPath is null or empty");
|
||||
return;
|
||||
}
|
||||
|
||||
var nfoContent = File.ReadAllText(ctn.episodeNfoPath);
|
||||
|
||||
var episodeXml = new XmlDocument();
|
||||
episodeXml.LoadXml(nfoContent);
|
||||
|
||||
var episodeNumberNode = episodeXml.SelectSingleNode("//episode");
|
||||
|
||||
if (episodeNumberNode == null)
|
||||
{
|
||||
_logger.LogError("episodeNumberNode is null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(episodeNumberNode.InnerText, out var episodeNumber))
|
||||
{
|
||||
_logger.LogError("episodeNumber is null");
|
||||
return;
|
||||
}
|
||||
|
||||
ctn.episodeNumber = episodeNumber;
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(requestPath))
|
||||
{
|
||||
if (requestSeasonNumber != null)
|
||||
{
|
||||
if (requestEpisodeNumber != null)
|
||||
{
|
||||
if (episodeNumber != requestEpisodeNumber)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("开始处理 Episode");
|
||||
|
||||
var isLockedNode = episodeXml.SelectSingleNode("//locked");
|
||||
|
||||
if (isLockedNode != null && isLockedNode.InnerText == "true" && ignoreLocked)
|
||||
{
|
||||
_logger.LogInformation("episodeNfo is locked");
|
||||
return;
|
||||
}
|
||||
|
||||
var isCompletedNode = episodeXml.SelectSingleNode("//completed");
|
||||
|
||||
if (isCompletedNode != null && isCompletedNode.InnerText == "true" && ignoreCompleted)
|
||||
{
|
||||
_logger.LogInformation("episodeNfo is completed");
|
||||
return;
|
||||
}
|
||||
|
||||
var episodeInfo = await GetTmdbEpisode(ctn);
|
||||
|
||||
if (episodeInfo == null)
|
||||
{
|
||||
_logger.LogError("episodeInfo is null");
|
||||
return;
|
||||
}
|
||||
|
||||
var titleNode = episodeXml.SelectSingleNode("//title");
|
||||
if (titleNode != null && episodeInfo["name"] != null)
|
||||
{
|
||||
_logger.LogInformation("episodeInfo: {episodeInfo}", episodeInfo["name"]);
|
||||
titleNode.InnerXml = $"<![CDATA[{episodeInfo["name"]}]]>";
|
||||
}
|
||||
|
||||
var plotNode = episodeXml.SelectSingleNode("//plot");
|
||||
if (plotNode != null && episodeInfo["overview"] != null)
|
||||
{
|
||||
_logger.LogInformation("episodeInfo: {episodeInfo}", episodeInfo["overview"]);
|
||||
plotNode.InnerXml = $"<![CDATA[{episodeInfo["overview"]}]]>";
|
||||
}
|
||||
|
||||
var outlineNode = episodeXml.SelectSingleNode("//outline");
|
||||
if (outlineNode != null && episodeInfo["overview"] != null)
|
||||
{
|
||||
_logger.LogInformation("episodeInfo: {episodeInfo}", episodeInfo["overview"]);
|
||||
outlineNode.InnerXml = $"<![CDATA[{episodeInfo["overview"]}]]>";
|
||||
}
|
||||
|
||||
var actors = episodeXml.SelectNodes("//actor");
|
||||
if (actors != null)
|
||||
{
|
||||
foreach (XmlNode actor in actors)
|
||||
{
|
||||
await HandleActor(actor);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("tmdbId: {tmdbId}, name: {name}", ctn.tvId, episodeInfo["name"]?.ToString());
|
||||
|
||||
if (isLockedNode != null)
|
||||
{
|
||||
isLockedNode.InnerText = "true";
|
||||
}
|
||||
else
|
||||
{
|
||||
isLockedNode = episodeXml.CreateElement("locked");
|
||||
episodeXml.DocumentElement?.AppendChild(isLockedNode);
|
||||
isLockedNode.InnerText = "true";
|
||||
}
|
||||
|
||||
if (isCompletedNode != null)
|
||||
{
|
||||
isCompletedNode.InnerText = "true";
|
||||
}
|
||||
else
|
||||
{
|
||||
isCompletedNode = episodeXml.CreateElement("completed");
|
||||
episodeXml.DocumentElement?.AppendChild(isCompletedNode);
|
||||
isCompletedNode.InnerText = "true";
|
||||
}
|
||||
|
||||
// 保存
|
||||
episodeXml.Save(ctn.episodeNfoPath);
|
||||
}
|
||||
|
||||
async Task<string?> GetTmdbPersonName(
|
||||
string path,
|
||||
int? tmdbId = null,
|
||||
string? name = null
|
||||
)
|
||||
{
|
||||
if (tmdbId != null)
|
||||
{
|
||||
var person = await GetTmdbPerson(path, tmdbId.Value);
|
||||
return person?["name"]?.ToString() ?? name;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(name) == false)
|
||||
{
|
||||
// 如果名称是中文直接返回
|
||||
if (Regex.IsMatch(name, "^[\u4e00-\u9fa5]+$"))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
var person = await GetTmdbPersonSearch(path, name);
|
||||
return person?["name"]?.ToString() ?? name;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var a = await GetTmdbTv(tvNfo, tmdbId);
|
||||
async Task HandleActor(XmlNode actor)
|
||||
{
|
||||
var nameNode = actor.SelectSingleNode("name");
|
||||
var tmdbIdNode = actor.SelectSingleNode("tmdbid");
|
||||
if (nameNode != null || tmdbIdNode != null)
|
||||
{
|
||||
var name = await GetTmdbPersonName(
|
||||
tvFolder,
|
||||
int.TryParse(tmdbIdNode?.InnerText, out var actorTmdbId) ? actorTmdbId : null,
|
||||
nameNode?.InnerText
|
||||
);
|
||||
|
||||
_logger.LogInformation("ChineseNfoRegistry.Job() tmdbId: {tmdbId}, name: {name}", tmdbId,a?["name"]?.ToString());
|
||||
return [];
|
||||
if (name == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (nameNode != null)
|
||||
{
|
||||
nameNode.InnerXml = $"<![CDATA[{name}]]>";
|
||||
}
|
||||
else
|
||||
{
|
||||
actor.AppendChild(new XmlDocument().CreateElement("name"));
|
||||
nameNode = actor.SelectSingleNode("name");
|
||||
nameNode!.InnerXml = $"<![CDATA[{name}]]>";
|
||||
}
|
||||
|
||||
_logger.LogInformation("actor: {actor}", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<JObject?> GetTmdbTv(
|
||||
string path,
|
||||
int tmdbId,
|
||||
TheMovieDbLanguage language = TheMovieDbLanguage.ZhCn)
|
||||
int tmdbId)
|
||||
{
|
||||
var record = await _freeSql.Select<TheMovieDbRecord>()
|
||||
.Where(x => x.Type == TheMovieDbType.Tv)
|
||||
.Where(x => x.UniqueId == tmdbId.ToString())
|
||||
.FirstAsync();
|
||||
|
||||
@@ -154,11 +626,10 @@ public class ChineseNfoRegistry : Registry, IChineseNfoRegistry
|
||||
record = new TheMovieDbRecord
|
||||
{
|
||||
Type = TheMovieDbType.Tv,
|
||||
UniqueId = tmdbId.ToString(),
|
||||
Language = language
|
||||
UniqueId = tmdbId.ToString()
|
||||
};
|
||||
|
||||
const string tvUrl = "/3/tv/{0}?api_key=e28e1bc408db7adefc8bacce225c5085&language=zh-CN";
|
||||
string tvUrl = "/3/tv/{0}?api_key=e28e1bc408db7adefc8bacce225c5085&language=zh-CN";
|
||||
var requestUrl = string.Format(tvUrl, tmdbId);
|
||||
|
||||
try
|
||||
@@ -167,31 +638,266 @@ public class ChineseNfoRegistry : Registry, IChineseNfoRegistry
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"{requestUrl} & {path} & {response.StatusCode}");
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbTv() 接口调用失败 {requestUrl} & {path} & {response.StatusCode}", requestUrl, path, response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var str = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var json = JObject.Parse(str);
|
||||
var name = json["name"];
|
||||
// 判断是否包含中文字符
|
||||
var isChinese = name != null && name.ToString().Any(c => c >= 0x4E00 && c <= 0x9FA5);
|
||||
if (language == TheMovieDbLanguage.ZhCn && !isChinese)
|
||||
{
|
||||
_logger.LogWarning("ChineseNfoRegistry.GetTmdbTv() name: {name}, 无中文文字, 降级到繁体字", name);
|
||||
// 降级到繁体
|
||||
return await GetTmdbTv(path, tmdbId, TheMovieDbLanguage.ZhTw);
|
||||
}
|
||||
|
||||
record.Json = str;
|
||||
_freeSql.Insert(record);
|
||||
await _freeSql.Insert(record).ExecuteIdentityAsync();
|
||||
|
||||
_logger.LogInformation("ChineseNfoRegistry.GetTmdbTv() 接口调用, 休眠 1S");
|
||||
await Task.Delay(1000);
|
||||
|
||||
return json;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"{requestUrl} & {path} \r {e}");
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbTv() 接口调用失败 {requestUrl} & {path} \r {e}", requestUrl, path, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<JObject?> GetTmdbPerson(
|
||||
string path,
|
||||
int tmdbId)
|
||||
{
|
||||
var record = await _freeSql.Select<TheMovieDbRecord>()
|
||||
.Where(x => x.Type == TheMovieDbType.Person)
|
||||
.Where(x => x.UniqueId == tmdbId.ToString())
|
||||
.FirstAsync();
|
||||
|
||||
if (record != null)
|
||||
{
|
||||
return JObject.Parse(record.Json);
|
||||
}
|
||||
|
||||
record = new TheMovieDbRecord
|
||||
{
|
||||
Type = TheMovieDbType.Person,
|
||||
UniqueId = tmdbId.ToString(),
|
||||
Language = TheMovieDbLanguage.ZhCn
|
||||
};
|
||||
|
||||
var requestUrl = string.Format("/3/person/{0}?api_key=e28e1bc408db7adefc8bacce225c5085&language=zh-CN", tmdbId);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _apiClient.GetAsync(requestUrl);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbPerson() 接口调用失败 {requestUrl} & {path} & {response.StatusCode}", requestUrl, path, response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var str = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var json = JObject.Parse(str);
|
||||
|
||||
record.Json = str;
|
||||
await _freeSql.Insert(record).ExecuteIdentityAsync();
|
||||
|
||||
_logger.LogInformation("ChineseNfoRegistry.GetTmdbPerson() 接口调用, 休眠 1S");
|
||||
await Task.Delay(1000);
|
||||
|
||||
return json;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbPerson() 接口调用失败 {requestUrl} & {path} \r {e}", requestUrl, path, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<JObject?> GetTmdbPersonSearch(
|
||||
string path,
|
||||
string name)
|
||||
{
|
||||
var record = await _freeSql.Select<TheMovieDbRecord>()
|
||||
.Where(x => x.Type == TheMovieDbType.Person)
|
||||
.Where(x => x.UniqueId == name)
|
||||
.FirstAsync();
|
||||
|
||||
if (record != null)
|
||||
{
|
||||
return JObject.Parse(record.Json);
|
||||
}
|
||||
|
||||
record = new TheMovieDbRecord
|
||||
{
|
||||
Type = TheMovieDbType.Person,
|
||||
UniqueId = name
|
||||
};
|
||||
|
||||
var requestUrl = string.Format("/3/search/person?api_key=e28e1bc408db7adefc8bacce225c5085&language=zh-CN&query={0}", name);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _apiClient.GetAsync(requestUrl);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbPersonSearch() 接口调用失败 {requestUrl} & {path} & {response.StatusCode}", requestUrl, path, response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var str = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var json = JObject.Parse(str);
|
||||
|
||||
var results = json["results"];
|
||||
|
||||
if (results != null)
|
||||
{
|
||||
foreach (var result in results.Cast<JObject>())
|
||||
{
|
||||
record.Json = result.ToString();
|
||||
await _freeSql.Insert(record).ExecuteIdentityAsync();
|
||||
|
||||
_logger.LogInformation("ChineseNfoRegistry.GetTmdbPersonSearch() 接口调用, 休眠 1S");
|
||||
await Task.Delay(1000);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbPersonSearch() 接口调用失败 {requestUrl} & {path} \r {e}", requestUrl, path, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<byte[]?> GetTmdbImage(
|
||||
string path,
|
||||
string posterPath)
|
||||
{
|
||||
var requestUrl = string.Format("/t/p/w1280/{0}", posterPath);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _imageClient.GetAsync(requestUrl);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbImage() 接口调用失败 {requestUrl} & {path} & {response.StatusCode}", requestUrl, path, response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsByteArrayAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbImage() 接口调用失败 {requestUrl} & {path} \r {e}", requestUrl, path, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<JObject?> GetTmdbSeason(ChineseNfoContent ctn)
|
||||
{
|
||||
if (ctn.seasonNumber == 0 || string.IsNullOrEmpty(ctn.seasonNfoPath))
|
||||
{
|
||||
_logger.LogError("seasonNfoPath is null or empty");
|
||||
return null;
|
||||
}
|
||||
|
||||
var uniqueId = $"{ctn.tvId}-{ctn.seasonNumber}";
|
||||
|
||||
var record = await _freeSql.Select<TheMovieDbRecord>()
|
||||
.Where(x => x.Type == TheMovieDbType.Season)
|
||||
.Where(x => x.UniqueId == uniqueId)
|
||||
.FirstAsync();
|
||||
|
||||
if (record != null)
|
||||
{
|
||||
return JObject.Parse(record.Json);
|
||||
}
|
||||
|
||||
record = new TheMovieDbRecord
|
||||
{
|
||||
Type = TheMovieDbType.Season,
|
||||
UniqueId = uniqueId
|
||||
};
|
||||
|
||||
var requestUrl = string.Format("/3/tv/{0}/season/{1}?api_key=e28e1bc408db7adefc8bacce225c5085&language=zh-CN", ctn.tvId, ctn.seasonNumber);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _apiClient.GetAsync(requestUrl);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbSeason() 接口调用失败 {requestUrl} & {path} & {response.StatusCode}", requestUrl, ctn.tvNfoPath, response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var str = await response.Content.ReadAsStringAsync();
|
||||
|
||||
record.Json = str;
|
||||
await _freeSql.Insert(record).ExecuteIdentityAsync();
|
||||
|
||||
_logger.LogInformation("ChineseNfoRegistry.GetTmdbSeason() 接口调用, 休眠 1S");
|
||||
await Task.Delay(1000);
|
||||
|
||||
return JObject.Parse(str);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbSeason() 接口调用失败 {requestUrl} & {path} \r {e}", requestUrl, ctn.tvNfoPath, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<JObject?> GetTmdbEpisode(ChineseNfoContent ctn)
|
||||
{
|
||||
var uniqueId = $"{ctn.tvId}-{ctn.seasonNumber}-{ctn.episodeNumber}";
|
||||
|
||||
var record = await _freeSql.Select<TheMovieDbRecord>()
|
||||
.Where(x => x.Type == TheMovieDbType.Episode)
|
||||
.Where(x => x.UniqueId == uniqueId)
|
||||
.FirstAsync();
|
||||
|
||||
if (record != null)
|
||||
{
|
||||
return JObject.Parse(record.Json);
|
||||
}
|
||||
|
||||
record = new TheMovieDbRecord
|
||||
{
|
||||
Type = TheMovieDbType.Episode,
|
||||
UniqueId = uniqueId
|
||||
};
|
||||
|
||||
var requestUrl = string.Format("/3/tv/{0}/season/{1}/episode/{2}?api_key=e28e1bc408db7adefc8bacce225c5085&language=zh-CN", ctn.tvId, ctn.seasonNumber, ctn.episodeNumber);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _apiClient.GetAsync(requestUrl);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbEpisode() 接口调用失败 {requestUrl} & {path} & {response.StatusCode}", requestUrl, ctn.episodeNfoPath, response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var str = await response.Content.ReadAsStringAsync();
|
||||
|
||||
record.Json = str;
|
||||
await _freeSql.Insert(record).ExecuteIdentityAsync();
|
||||
|
||||
_logger.LogInformation("ChineseNfoRegistry.GetTmdbEpisode() 接口调用, 休眠 1S");
|
||||
await Task.Delay(1000);
|
||||
|
||||
return JObject.Parse(str);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("ChineseNfoRegistry.GetTmdbEpisode() 接口调用失败 {requestUrl} & {path} \r {e}", requestUrl, ctn.episodeNfoPath, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -199,6 +905,7 @@ public class ChineseNfoRegistry : Registry, IChineseNfoRegistry
|
||||
|
||||
public class TheMovieDbRecord
|
||||
{
|
||||
[Column(IsPrimary = true, IsIdentity = true)]
|
||||
public long Id { get; set; }
|
||||
|
||||
public TheMovieDbType Type { get; set; }
|
||||
@@ -207,8 +914,7 @@ public class TheMovieDbRecord
|
||||
|
||||
public TheMovieDbLanguage Language { get; set; }
|
||||
|
||||
[Column(DbType = "text")]
|
||||
public string Json { get; set; } = string.Empty;
|
||||
[Column(DbType = "text")] public string Json { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public enum TheMovieDbType
|
||||
@@ -216,6 +922,7 @@ public enum TheMovieDbType
|
||||
Tv = 1,
|
||||
Season = 2,
|
||||
Episode = 3,
|
||||
Person = 4
|
||||
}
|
||||
|
||||
public enum TheMovieDbLanguage
|
||||
@@ -223,3 +930,18 @@ public enum TheMovieDbLanguage
|
||||
ZhCn = 1,
|
||||
ZhTw = 2
|
||||
}
|
||||
|
||||
public class ChineseNfoContent
|
||||
{
|
||||
public int tvId { get; set; }
|
||||
|
||||
public string tvNfoPath { get; set; } = string.Empty;
|
||||
|
||||
public int seasonNumber { get; set; }
|
||||
|
||||
public string seasonNfoPath { get; set; } = string.Empty;
|
||||
|
||||
public int episodeNumber { get; set; }
|
||||
|
||||
public string episodeNfoPath { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
using Core;
|
||||
using FluentScheduler;
|
||||
using Interface.Jobs;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Service.Jobs;
|
||||
|
||||
public enum DiskNotifyType
|
||||
{
|
||||
Action,
|
||||
Sleep
|
||||
}
|
||||
|
||||
public class DiskActionMonitorRegistry : Registry, IDiskActionMonitorRegistry
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private static Dictionary<string, int> _lastReadCount = new();
|
||||
private static Dictionary<string, int> _lastWriteCount = new();
|
||||
private readonly ILogger<DiskActionMonitorRegistry> _logger;
|
||||
private static readonly Dictionary<string, List<(DateTime, DiskNotifyType)>> LatestNotifyTimes = new();
|
||||
private static readonly Dictionary<string, DateTime> LastChangedTimes = new();
|
||||
|
||||
public DiskActionMonitorRegistry(IConfiguration configuration, ILogger<DiskActionMonitorRegistry> logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
Schedule(Job).ToRunEvery(5).Seconds();
|
||||
}
|
||||
|
||||
public async void Job()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JobExecute();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "DiskActionMonitorRegistry.Job() error");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task JobExecute()
|
||||
{
|
||||
var filePath = _configuration["DiskActionMonitor:FilePath"] ?? throw new ArgumentNullException($"DiskActionMonitor:FilePath");
|
||||
var disks = _configuration
|
||||
.GetSection("DiskActionMonitor:Disks")
|
||||
.GetChildren()
|
||||
.Select(x => x.Value);
|
||||
|
||||
|
||||
foreach (var disk in disks)
|
||||
{
|
||||
if (string.IsNullOrEmpty(disk))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_lastReadCount.ContainsKey(disk))
|
||||
{
|
||||
_lastReadCount.Add(disk, 0);
|
||||
}
|
||||
|
||||
if (!_lastWriteCount.ContainsKey(disk))
|
||||
{
|
||||
_lastWriteCount.Add(disk, 0);
|
||||
}
|
||||
|
||||
if (!LatestNotifyTimes.ContainsKey(disk))
|
||||
{
|
||||
LatestNotifyTimes.Add(disk, new());
|
||||
}
|
||||
|
||||
if (!LastChangedTimes.ContainsKey(disk))
|
||||
{
|
||||
LastChangedTimes.Add(disk, DateTime.Now);
|
||||
}
|
||||
|
||||
var content = await File.ReadAllTextAsync(string.Format(filePath, disk));
|
||||
|
||||
var cols = content.Split(" ", StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var readCount = TryGetByIndex(cols, 2);
|
||||
var writeCount = TryGetByIndex(cols, 6);
|
||||
|
||||
if (!int.TryParse(readCount, out var read) || !int.TryParse(writeCount, out var write))
|
||||
{
|
||||
continue; // 读取失败
|
||||
}
|
||||
|
||||
var readDiff = read - _lastReadCount[disk];
|
||||
var writeDiff = write - _lastWriteCount[disk];
|
||||
|
||||
_lastReadCount[disk] = read;
|
||||
_lastWriteCount[disk] = write;
|
||||
|
||||
if (readDiff > 0 || writeDiff > 0)
|
||||
{
|
||||
_logger.LogInformation($"[{disk}] ReadDiff: {readDiff}, WriteDiff: {writeDiff}");
|
||||
}
|
||||
|
||||
if (readDiff > 10 || writeDiff > 10)
|
||||
{
|
||||
// 上一次通知的是休眠或者没有通知 发现有变换通知变化
|
||||
if (LatestNotifyTimes[disk].Any() == false
|
||||
|| LatestNotifyTimes[disk].Last().Item2 == DiskNotifyType.Sleep)
|
||||
{
|
||||
LatestNotifyTimes[disk].Add((DateTime.Now, DiskNotifyType.Action));
|
||||
|
||||
await WxNotify.SendDiskInfoAsync(@$"
|
||||
## 磁盘 <font color='info'> {disk} </font> 刚发生读写操作 🙋
|
||||
|
||||
> 当前累计读: <font color='warning'> {GetEasyReadNumber(read)} </font>、新增读: <font color='warning'> {GetEasyReadNumber(readDiff)} </font>
|
||||
> 当前累计写: <font color='warning'> {GetEasyReadNumber(write)} </font>、新增写: <font color='warning'> {GetEasyReadNumber(writeDiff)} </font>
|
||||
|
||||
当前时间: <font color='comment'> {DateTime.Now:yyyy-M-d H:m:s} </font>");
|
||||
}
|
||||
else if (DateTime.Now - LatestNotifyTimes[disk].Last().Item1 > TimeSpan.FromHours(1))
|
||||
{
|
||||
LatestNotifyTimes[disk].Add((DateTime.Now, DiskNotifyType.Action));
|
||||
|
||||
await WxNotify.SendDiskInfoAsync(@$"
|
||||
## 磁盘 <font color='info'> {disk} </font> 连续运行超一小时 👨💻
|
||||
|
||||
> 当前累计读: <font color='warning'> {GetEasyReadNumber(read)} </font>
|
||||
> 当前累计写: <font color='warning'> {GetEasyReadNumber(write)} </font>
|
||||
|
||||
当前时间: <font color='comment'> {DateTime.Now:yyyy-M-d H:m:s} </font>");
|
||||
}
|
||||
|
||||
_lastReadCount[disk] = read;
|
||||
_lastWriteCount[disk] = write;
|
||||
LastChangedTimes[disk] = DateTime.Now;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// 没有变化 上一次是启动通知且超过10分钟,通知休眠
|
||||
if (LatestNotifyTimes[disk].Any() == false
|
||||
|| (LatestNotifyTimes[disk].Last().Item2 == DiskNotifyType.Action
|
||||
&& DateTime.Now - LastChangedTimes[disk] > TimeSpan.FromMinutes(10)))
|
||||
{
|
||||
LatestNotifyTimes[disk].Add((DateTime.Now, DiskNotifyType.Sleep));
|
||||
|
||||
await WxNotify.SendDiskInfoAsync(@$"
|
||||
## 磁盘 <font color='info'> {disk} </font> 长时间无读写变化 😪
|
||||
|
||||
> 上一次读写时间:{LatestNotifyTimes[disk].Last(x => x.Item2 == DiskNotifyType.Action).Item1:yyyy-M-d H:m:s}
|
||||
|
||||
> 当前累计读: <font color='warning'> {GetEasyReadNumber(read)} </font>
|
||||
> 当前累计写: <font color='warning'> {GetEasyReadNumber(write)} </font>
|
||||
|
||||
当前时间: <font color='comment'> {DateTime.Now:yyyy-M-d H:m:s} </font>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string TryGetByIndex(string[] arr, int index) => index < arr.Length ? arr[index] : string.Empty;
|
||||
|
||||
private string GetEasyReadNumber(int number)
|
||||
{
|
||||
if (number < 10000)
|
||||
{
|
||||
return number.ToString();
|
||||
}
|
||||
|
||||
if (number < 100000000)
|
||||
{
|
||||
return $"{number / 10000M:0.##} 万";
|
||||
}
|
||||
|
||||
return $"{number / 100000000M:0.##} 亿";
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using FluentScheduler;
|
||||
using InfluxDB.Client;
|
||||
using InfluxDB.Client.Api.Domain;
|
||||
using InfluxDB.Client.Writes;
|
||||
using Interface.Jobs;
|
||||
|
||||
namespace Service.Jobs;
|
||||
|
||||
public class DiskMonitorRegistry : Registry, IDiskMonitorRegistry
|
||||
{
|
||||
public DiskMonitorRegistry()
|
||||
{
|
||||
Schedule(Job).ToRunNow().AndEvery(1).Hours();
|
||||
}
|
||||
|
||||
public void Job()
|
||||
{
|
||||
try
|
||||
{
|
||||
JobExecute();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void JobExecute()
|
||||
{
|
||||
// 执行 cmd 命令 获取执行结果
|
||||
var command = "df -h";
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new()
|
||||
{
|
||||
FileName = "/bin/bash",
|
||||
Arguments = $"-c \"{command}\"",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
var result = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
var format = FormatResult(result);
|
||||
|
||||
WriteToInfluxDB(format);
|
||||
}
|
||||
|
||||
private string FormatResult(string result)
|
||||
{
|
||||
var lines = result.Split("\n");
|
||||
var sb = new StringBuilder();
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var cols = line.Split(" ", StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (line.Contains("/host/wd/"))
|
||||
{
|
||||
sb.AppendLine($"{cols[5].Substring("/host".Length)},{cols[1]},{cols[4].TrimEnd('%')},{cols[3]}");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void WriteToInfluxDB(string result)
|
||||
{
|
||||
var lines = result.Split("\n");
|
||||
|
||||
using var client = new InfluxDBClient("http://influxdb:8086", "BD4A71llb9_XbCA5mmKDbc_yTYwadPPLwyk4nAQ0l_yR_WJmw_-dMOWIs0KlS7-pZtHot_HrejY5GcOohKElmA==");
|
||||
using var writeApi = client.GetWriteApi();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
Console.WriteLine(line);
|
||||
|
||||
var cols = line.Split(",", StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if(cols.Length != 4) continue;
|
||||
|
||||
var path = cols[0];
|
||||
var totalSize = cols[1];
|
||||
var usedPercent = cols[2];
|
||||
var available = cols[3];
|
||||
|
||||
var point = PointData
|
||||
.Measurement("disk_usage")
|
||||
.Tag("path", path)
|
||||
.Field("total_size", totalSize)
|
||||
.Field("used_percent", double.Parse(usedPercent))
|
||||
.Field("available", available)
|
||||
.Timestamp(DateTime.UtcNow, WritePrecision.Ns);
|
||||
|
||||
writeApi.WritePoint(point, "def-bucket", "def-org");
|
||||
Console.WriteLine($"[DiskMonitor] Write point: {point.ToLineProtocol()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using Core;
|
||||
using FluentScheduler;
|
||||
using Interface.Jobs;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Service.Jobs;
|
||||
|
||||
public class HealthyTaskRegistry : Registry, IHealthyTaskRegistry
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public HealthyTaskRegistry(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
Schedule(Job).ToRunNow().AndEvery(2).Minutes();
|
||||
}
|
||||
|
||||
public async void Job()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JobExecute();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
await WxNotify.SendCommonAsync($"HealthyTaskRegistry.Job() error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task JobExecute()
|
||||
{
|
||||
// 获取 配置 是个 KV 集合
|
||||
var configs = _configuration.GetSection("HealthyTasks");
|
||||
|
||||
// 遍历配置
|
||||
foreach (var item in configs.GetChildren())
|
||||
{
|
||||
// 获取配置的 containerName 和 url
|
||||
var containerName = item["ContainerName"];
|
||||
var url = item["Url"];
|
||||
|
||||
// 执行
|
||||
await ExecuteItem(containerName!, url!);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteItem(string containerName, string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = new HttpClient();
|
||||
|
||||
// basic auth
|
||||
string authInfo = Convert.ToBase64String(Encoding.ASCII.GetBytes("suncheng:SCsunch940622"));
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authInfo);
|
||||
|
||||
var response = await client.GetAsync(url);
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new Exception($"StatusCode: {response.StatusCode}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"ContainerName: {containerName}, Url: {url}, StatusCode: {response.StatusCode}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
await WxNotify.SendCommonAsync($"ExecuteItem {containerName} error: {e.Message}");
|
||||
await ExecuteReboot(containerName);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteReboot(string containerName)
|
||||
{
|
||||
var command = $"docker restart {containerName}";
|
||||
|
||||
var (responseCode, message) = await Command.ExecAsync(command);
|
||||
|
||||
if (responseCode != HttpStatusCode.OK)
|
||||
{
|
||||
await WxNotify.SendCommonAsync($"ExecuteReboot {containerName} error: {responseCode} message: {message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
await WxNotify.SendCommonAsync($"ContainerName: {containerName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
using System.Text;
|
||||
using Core;
|
||||
using FluentScheduler;
|
||||
using Interface.Jobs;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Service.Jobs;
|
||||
|
||||
public class LogTotalNotifyJobRegistry : Registry, ILogTotalNotifyJobRegistry
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public LogTotalNotifyJobRegistry(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
Schedule(Job).ToRunEvery(1).Days().At(8, 30);
|
||||
}
|
||||
|
||||
public async void Job()
|
||||
{
|
||||
try
|
||||
{
|
||||
// await JobExecute();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
await WxNotify.SendCommonAsync($"LogTotalNotifyJobRegistry.Job() error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task JobExecute()
|
||||
{
|
||||
var client = new HttpClient();
|
||||
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {_configuration["Grafana:Token"]}");
|
||||
var requestBody =
|
||||
"""
|
||||
{
|
||||
"queries": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "edf5cwf6n6i2oe"
|
||||
},
|
||||
"editorMode": "builder",
|
||||
"expr": "sum by(container_name) (count_over_time({compose_project=~\"dockers|immich|nasrobot|elasticsearch|webui-docker\"} [1h]))",
|
||||
"queryType": "range",
|
||||
"refId": "A",
|
||||
"datasourceId": 2,
|
||||
"intervalMs": 3600000
|
||||
}
|
||||
],
|
||||
"from": "1708963200000",
|
||||
"to": "1709049600000"
|
||||
}
|
||||
""";
|
||||
|
||||
requestBody = requestBody.Replace("1708963200000", DateTime.Today.AddDays(-2).ToUnixTimeMilliseconds().ToString());
|
||||
requestBody = requestBody.Replace("1709049600000", DateTime.Today.AddDays(-1).ToUnixTimeMilliseconds().ToString());
|
||||
|
||||
var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
|
||||
var response = await client.PostAsync(_configuration["Grafana:LokiUrl"], content);
|
||||
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var jObject = JObject.Parse(result);
|
||||
var frames = jObject["results"]?["A"]?["frames"];
|
||||
|
||||
if (frames == null)
|
||||
{
|
||||
throw new Exception("frames is null");
|
||||
}
|
||||
|
||||
var msg = new StringBuilder();
|
||||
msg.AppendLine($"## {DateTime.Today.AddDays(-1):yyyy-MM-dd}日志明细如下:");
|
||||
var total = 0;
|
||||
var kv = new Dictionary<string, int>();
|
||||
foreach (var item in frames)
|
||||
{
|
||||
var name = item["schema"]?["fields"]?.LastOrDefault()?["labels"]?["container_name"]?.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var values = item["data"]?["values"]?.LastOrDefault()?.ToObject<int[]>();
|
||||
|
||||
var value = values?.Sum() ?? 0;
|
||||
total += value;
|
||||
kv[name] = value;
|
||||
}
|
||||
|
||||
foreach (var (key, value) in kv.OrderByDescending(x => x.Value))
|
||||
{
|
||||
msg.AppendLine($"<font color='info'>**{key}**</font>: <font color='warning'>{value}</font>");
|
||||
}
|
||||
|
||||
msg.AppendLine("");
|
||||
msg.AppendLine($"> **总计**: <font color='warning'>{total}</font>");
|
||||
|
||||
await WxNotify.SendCommonAsync(msg.ToString());
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http.Json;
|
||||
using Core;
|
||||
using FluentScheduler;
|
||||
using Interface.Jobs;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Service.Jobs;
|
||||
|
||||
public class RSyncTaskRegistry : Registry, IRSyncTaskRegistry
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public RSyncTaskRegistry(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
Schedule(Job).ToRunEvery(1).Days().At(10, 0);
|
||||
}
|
||||
|
||||
public async void Job()
|
||||
{
|
||||
try
|
||||
{
|
||||
// await JobExecute();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
await WxNotify.SendCommonAsync($"RSyncTaskRegistry.Job() error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task JobExecute()
|
||||
{
|
||||
var config = _configuration.GetSection("SyncTask");
|
||||
|
||||
foreach (var item in config.GetSection("SyncPaths").GetChildren())
|
||||
{
|
||||
var source = Path.Combine(config["SourceRoot"]!, item["Source"]!);
|
||||
var isDeleteAfter = item["DeleteAfter"] == "true";
|
||||
await ExecuteItem(source, config["TargetRoot"]!, item["Target"]!, isDeleteAfter);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteItem(string source, string remote, string destination, bool isDeleteAfter)
|
||||
{
|
||||
var logName = $"rclone_output_{destination.Replace("/", "_")}{DateTime.Now:yyMMddHHmm}.log";
|
||||
|
||||
var commands = new[]
|
||||
{
|
||||
$"rclone sync " +
|
||||
$"{source} " +
|
||||
$"{remote}:{destination} " +
|
||||
$"--fast-list " +
|
||||
$"--size-only " +
|
||||
$"{(isDeleteAfter ? "--delete-after" : "--delete-excluded")} " +
|
||||
$"> /wd/logs/{logName} 2>&1"
|
||||
};
|
||||
|
||||
await WxNotify.SendCommonAsync($@"RSyncTaskRegistry.ExecuteItem()
|
||||
|
||||
`{commands[0]}`
|
||||
|
||||
> {DateTime.Now:yyyy-MM-dd HH:mm:ss}
|
||||
");
|
||||
|
||||
_ = Command.ExecAsync(commands);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System.Net;
|
||||
using Core;
|
||||
using FluentScheduler;
|
||||
using Interface.Jobs;
|
||||
|
||||
namespace Service.Jobs;
|
||||
|
||||
public class ShutdownRegistry : Registry, IShutdownRegistry
|
||||
{
|
||||
public ShutdownRegistry()
|
||||
{
|
||||
Schedule(Job).ToRunEvery(1).Days().At(23, 30);
|
||||
}
|
||||
|
||||
public async void Job()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JobExecute();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
await WxNotify.SendCommonAsync($"ShutdownRegistry.Job() error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task JobExecute()
|
||||
{
|
||||
var command = "shutdown 9";
|
||||
|
||||
var (responseCode, msg) = await Command.ExecAsync(command);
|
||||
|
||||
var wxMsg = $@"
|
||||
# 定时关机指令执行
|
||||
|
||||
> 执行结果:{responseCode}
|
||||
> 执行消息:{msg}
|
||||
|
||||
如需取消关机请点击:[取消关机](http://suncheng.online:35642/api/JobTrigger/CancelShutdown)
|
||||
|
||||
";
|
||||
|
||||
await WxNotify.SendCommonAsync(wxMsg);
|
||||
|
||||
if (responseCode == HttpStatusCode.OK)
|
||||
{
|
||||
// 每三分钟提醒一次
|
||||
_ = Task.Delay(3 * 60 * 1000).ContinueWith(async _ => await WxNotify.SendCommonAsync(wxMsg));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CancelShutdown()
|
||||
{
|
||||
var command = "shutdown -c";
|
||||
|
||||
var (responseCode, _) = await Command.ExecAsync(command);
|
||||
|
||||
await WxNotify.SendCommonAsync($"取消关机指令执行完成 {responseCode}");
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using System.Net;
|
||||
using Core;
|
||||
using FluentScheduler;
|
||||
using Interface.Jobs;
|
||||
|
||||
namespace Service.Jobs;
|
||||
|
||||
public class StartupRegistry : Registry, IStartupRegistry
|
||||
{
|
||||
public StartupRegistry()
|
||||
{
|
||||
Schedule(Job).ToRunNow();
|
||||
}
|
||||
|
||||
public async void Job()
|
||||
{
|
||||
try
|
||||
{
|
||||
// await JobExecute();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
await WxNotify.SendCommonAsync($"StartupRegistry.Job() error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task JobExecute()
|
||||
{
|
||||
var commands = new[]
|
||||
{
|
||||
"chmod 777 /var/run/docker.sock",
|
||||
};
|
||||
|
||||
var (responseCode, _) = await Command.ExecAsync(commands);
|
||||
|
||||
if (responseCode != HttpStatusCode.OK)
|
||||
{
|
||||
await WxNotify.SendCommonAsync($"StartupRegistry.Job() error: {responseCode}");
|
||||
}
|
||||
else
|
||||
{
|
||||
await WxNotify.SendCommonAsync("StartupRegistry.Job() success");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +1,125 @@
|
||||
using Interface.Jobs;
|
||||
using System.Text.Json.Nodes;
|
||||
using Core;
|
||||
using Interface.Jobs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace WebApi.Controllers;
|
||||
|
||||
public class JobTriggerController : BaseController
|
||||
{
|
||||
private readonly ILogTotalNotifyJobRegistry _logTotalNotifyJobRegistry;
|
||||
private readonly IDiskActionMonitorRegistry _diskActionMonitorRegistry;
|
||||
private readonly IHealthyTaskRegistry _healthyTaskRegistry;
|
||||
private readonly IRSyncTaskRegistry _rSyncTaskRegistry;
|
||||
private readonly IStartupRegistry _startupRegistry;
|
||||
private readonly IShutdownRegistry _shutdownRegistry;
|
||||
private readonly IChineseNfoRegistry _chineseNfoRegistry;
|
||||
private readonly IDiskMonitorRegistry _diskMonitorRegistry;
|
||||
|
||||
private readonly ILogger<JobTriggerController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public JobTriggerController(
|
||||
ILogTotalNotifyJobRegistry logTotalNotifyJobRegistry,
|
||||
IDiskActionMonitorRegistry diskActionMonitorRegistry,
|
||||
IHealthyTaskRegistry healthyTaskRegistry,
|
||||
IRSyncTaskRegistry rSyncTaskRegistry,
|
||||
IStartupRegistry startupRegistry,
|
||||
IShutdownRegistry shutdownRegistry,
|
||||
IChineseNfoRegistry chineseNfoRegistry,
|
||||
IDiskMonitorRegistry diskMonitorRegistry)
|
||||
ILogger<JobTriggerController> logger)
|
||||
{
|
||||
_logTotalNotifyJobRegistry = logTotalNotifyJobRegistry;
|
||||
_diskActionMonitorRegistry = diskActionMonitorRegistry;
|
||||
_healthyTaskRegistry = healthyTaskRegistry;
|
||||
_rSyncTaskRegistry = rSyncTaskRegistry;
|
||||
_startupRegistry = startupRegistry;
|
||||
_shutdownRegistry = shutdownRegistry;
|
||||
_chineseNfoRegistry = chineseNfoRegistry;
|
||||
_diskMonitorRegistry = diskMonitorRegistry;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
public string LogTotalNotify()
|
||||
public string ConvertChineseNfo(
|
||||
bool ignoreLocked = true,
|
||||
bool ignoreCompleted = true)
|
||||
{
|
||||
_logTotalNotifyJobRegistry.Job();
|
||||
_chineseNfoRegistry.Job(ignoreLocked: ignoreLocked, ignoreCompleted: ignoreCompleted);
|
||||
|
||||
return "OK";
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<string> SonarrChangedConvertChineseNfo()
|
||||
{
|
||||
|
||||
var body = Request.Body;
|
||||
|
||||
using var reader = new StreamReader(body);
|
||||
var text = await reader.ReadToEndAsync();
|
||||
|
||||
var json = JsonConvert.DeserializeObject<JObject>(text);
|
||||
|
||||
var eventType = json?["eventType"]?.ToString();
|
||||
|
||||
if (eventType != "Download")
|
||||
{
|
||||
return "OK";
|
||||
}
|
||||
|
||||
_logger.LogInformation("SonarrChangedConvertChineseNfo() called");
|
||||
await Task.Delay(10_000);
|
||||
|
||||
var seasonNumber = json?["episodes"]?[0]?["seasonNumber"]?.ToObject<int>();
|
||||
var episodeNumber = json?["episodes"]?[0]?["episodeNumber"]?.ToObject<int>();
|
||||
var path = json?["series"]?["path"]?.ToString();
|
||||
|
||||
await WxNotify.SendCommonAsync($"SonarrChangedConvertChineseNfo() path: {path}, seasonNumber: {seasonNumber}, episodeNumber: {episodeNumber}");
|
||||
|
||||
_logger.LogInformation("SonarrChangedConvertChineseNfo() path: {path}, seasonNumber: {seasonNumber}, episodeNumber: {episodeNumber}", path, seasonNumber, episodeNumber);
|
||||
|
||||
if (string.IsNullOrEmpty(path) || seasonNumber == null || episodeNumber == null)
|
||||
{
|
||||
return "OK";
|
||||
}
|
||||
|
||||
await Task.Delay(10_000); // 休眠10S,防止文件还未能创建
|
||||
|
||||
_chineseNfoRegistry.Job(path: path, seasonNumber: seasonNumber, episodeNumber: episodeNumber, ignoreLocked: true, ignoreCompleted: true);
|
||||
|
||||
return "OK";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public string DiskActionMonitor()
|
||||
public string ConvertChineseNfoByPath(
|
||||
string path,
|
||||
int? seasonNumber = null,
|
||||
int? episodeNumber = null,
|
||||
bool ignoreLocked = true,
|
||||
bool ignoreCompleted = true)
|
||||
{
|
||||
_diskActionMonitorRegistry.Job();
|
||||
_chineseNfoRegistry.Job(path: path, seasonNumber: seasonNumber, episodeNumber: episodeNumber, ignoreLocked: ignoreLocked, ignoreCompleted: ignoreCompleted);
|
||||
|
||||
return "OK";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public string HealthyTask()
|
||||
public string MusicTagAsync()
|
||||
{
|
||||
_healthyTaskRegistry.Job();
|
||||
var musicPath = "/data/media/music";
|
||||
List<string> musicType = ["mp3", "flac", "m4a", "wav", "ape", "ogg", "wma", "alac", "aac", "dsd"];
|
||||
|
||||
return "OK";
|
||||
// 递归获取所有音乐文件
|
||||
var files = Directory.GetFiles(musicPath, "*.*", SearchOption.AllDirectories)
|
||||
.Where(f => musicType.Contains(Path.GetExtension(f).TrimStart('.').ToLower()))
|
||||
.ToList();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
_logger.LogInformation("Processing music file: {file}", file);
|
||||
|
||||
try
|
||||
{
|
||||
var fileTag = TagLib.File.Create(file);
|
||||
var artist = fileTag.Tag.FirstPerformer ?? "未知艺术家";
|
||||
var album = fileTag.Tag.Album ?? "未知专辑";
|
||||
var title = fileTag.Tag.Title ?? Path.GetFileNameWithoutExtension(file);
|
||||
var year = fileTag.Tag.Year > 0 ? fileTag.Tag.Year.ToString() : "未知年份";
|
||||
var genre = fileTag.Tag.FirstGenre ?? "未知风格";
|
||||
_logger.LogInformation("Tag info - Artist: {artist}, Album: {album}, Title: {title}, Year: {year}, Genre: {genre}", artist, album, title, year, genre);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
public string RSyncTask()
|
||||
catch (Exception e)
|
||||
{
|
||||
_rSyncTaskRegistry.Job();
|
||||
|
||||
return "OK";
|
||||
_logger.LogError(e, "Error processing music file: {file}", file);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public string Startup()
|
||||
{
|
||||
_startupRegistry.Job();
|
||||
|
||||
return "OK";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public string Shutdown()
|
||||
{
|
||||
_shutdownRegistry.Job();
|
||||
|
||||
return "OK";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public string CancelShutdown()
|
||||
{
|
||||
_shutdownRegistry.CancelShutdown();
|
||||
|
||||
return "OK";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public string ConvertChineseNfo(bool ignoreLocked = false, bool ignoreCompleted = false)
|
||||
{
|
||||
_chineseNfoRegistry.Job(ignoreLocked, ignoreCompleted);
|
||||
|
||||
return "OK";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public string DiskMonitor()
|
||||
{
|
||||
_diskMonitorRegistry.Job();
|
||||
|
||||
return "OK";
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,6 @@ namespace WebApi.Controllers;
|
||||
|
||||
public class NotifyController : BaseController
|
||||
{
|
||||
public NotifyController()
|
||||
{
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<string> Radarr()
|
||||
{
|
||||
@@ -21,40 +17,6 @@ public class NotifyController : BaseController
|
||||
|
||||
Console.WriteLine(text);
|
||||
|
||||
/*
|
||||
{
|
||||
"movie": {
|
||||
"id": 1,
|
||||
"title": "Test Title",
|
||||
"year": 1970,
|
||||
"releaseDate": "1970-01-01",
|
||||
"folderPath": "C:\\testpath",
|
||||
"tmdbId": 0,
|
||||
"tags": [
|
||||
"test-tag"
|
||||
]
|
||||
},
|
||||
"remoteMovie": {
|
||||
"tmdbId": 1234,
|
||||
"imdbId": "5678",
|
||||
"title": "Test title",
|
||||
"year": 1970
|
||||
},
|
||||
"release": {
|
||||
"quality": "Test Quality",
|
||||
"qualityVersion": 1,
|
||||
"releaseGroup": "Test Group",
|
||||
"releaseTitle": "Test Title",
|
||||
"indexer": "Test Indexer",
|
||||
"size": 9999999,
|
||||
"customFormatScore": 0
|
||||
},
|
||||
"eventType": "Test",
|
||||
"instanceName": "Radarr",
|
||||
"applicationUrl": ""
|
||||
}
|
||||
*/
|
||||
|
||||
var jsonObj = JsonConvert.DeserializeObject<JObject>(text);
|
||||
|
||||
var notify = @$"# Radarr通知:
|
||||
@@ -96,189 +58,6 @@ public class NotifyController : BaseController
|
||||
|
||||
Console.WriteLine(text);
|
||||
|
||||
/*
|
||||
{
|
||||
"series": {
|
||||
"id": 86,
|
||||
"title": "Demon Slayer: Kimetsu no Yaiba",
|
||||
"titleSlug": "demon-slayer-kimetsu-no-yaiba",
|
||||
"path": "/data/anime/Demon Slayer - Kimetsu no Yaiba",
|
||||
"tvdbId": 348545,
|
||||
"tvMazeId": 41469,
|
||||
"tmdbId": 85937,
|
||||
"imdbId": "tt9335498",
|
||||
"type": "anime",
|
||||
"year": 2019,
|
||||
"genres": [
|
||||
"Action",
|
||||
"Adventure",
|
||||
"Animation",
|
||||
"Anime",
|
||||
"Drama",
|
||||
"Fantasy",
|
||||
"Thriller"
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"coverType": "banner",
|
||||
"url": "/MediaCover/86/banner.jpg?lastWrite=638509998968281535",
|
||||
"remoteUrl": "https://artworks.thetvdb.com/banners/graphical/5ccd960cc3aa0.jpg"
|
||||
},
|
||||
{
|
||||
"coverType": "poster",
|
||||
"url": "/MediaCover/86/poster.jpg?lastWrite=638509998969561521",
|
||||
"remoteUrl": "https://artworks.thetvdb.com/banners/v4/series/348545/posters/60908d475f49a.jpg"
|
||||
},
|
||||
{
|
||||
"coverType": "fanart",
|
||||
"url": "/MediaCover/86/fanart.jpg?lastWrite=638509998971801497",
|
||||
"remoteUrl": "https://artworks.thetvdb.com/banners/fanart/original/5c93cbb2b60b6.jpg"
|
||||
},
|
||||
{
|
||||
"coverType": "clearlogo",
|
||||
"url": "/MediaCover/86/clearlogo.png?lastWrite=638509998972921485",
|
||||
"remoteUrl": "https://artworks.thetvdb.com/banners/v4/series/348545/clearlogo/611c7fa8222d6.png"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"anime"
|
||||
]
|
||||
},
|
||||
"episodes": [
|
||||
{
|
||||
"id": 2716,
|
||||
"episodeNumber": 2,
|
||||
"seasonNumber": 5,
|
||||
"title": "Water Hashira Giyu Tomioka's Pain",
|
||||
"overview": "Kagaya's Kasugai Crow suddenly appears in front of Tamayo and invites her to the Demon Slayer headquarters — even though she is a demon. Meanwhile, Tanjiro, who is recovering at the Butterfly Mansion, receives a letter from Kagaya...",
|
||||
"airDate": "2024-05-19",
|
||||
"airDateUtc": "2024-05-19T14:15:00Z",
|
||||
"seriesId": 86,
|
||||
"tvdbId": 10445764
|
||||
}
|
||||
],
|
||||
"release": {
|
||||
"quality": "WEBDL-1080p",
|
||||
"qualityVersion": 1,
|
||||
"releaseGroup": "ToonsHub",
|
||||
"releaseTitle": "[ToonsHub] Demon Slayer Kimetsu no Yaiba S05E02 1080p CR WEB-DL AAC2.0 x264 (Multi-Subs)",
|
||||
"indexer": "Knaben ",
|
||||
"size": 1395864320,
|
||||
"customFormatScore": 0,
|
||||
"customFormats": []
|
||||
},
|
||||
"downloadClient": "aria2",
|
||||
"downloadClientType": "Aria2",
|
||||
"downloadId": "915718C3A8A5B15BD3C32A2B05885953D96AFADD",
|
||||
"customFormatInfo": {
|
||||
"customFormats": [],
|
||||
"customFormatScore": 0
|
||||
},
|
||||
"eventType": "Grab",
|
||||
"instanceName": "Sonarr",
|
||||
"applicationUrl": ""
|
||||
}
|
||||
|
||||
|
||||
--------------------------
|
||||
|
||||
{
|
||||
"series": {
|
||||
"id": 86,
|
||||
"title": "Demon Slayer: Kimetsu no Yaiba",
|
||||
"titleSlug": "demon-slayer-kimetsu-no-yaiba",
|
||||
"path": "/data/anime/Demon Slayer - Kimetsu no Yaiba",
|
||||
"tvdbId": 348545,
|
||||
"tvMazeId": 41469,
|
||||
"tmdbId": 85937,
|
||||
"imdbId": "tt9335498",
|
||||
"type": "anime",
|
||||
"year": 2019,
|
||||
"genres": [
|
||||
"Action",
|
||||
"Adventure",
|
||||
"Animation",
|
||||
"Anime",
|
||||
"Drama",
|
||||
"Fantasy",
|
||||
"Thriller"
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"coverType": "banner",
|
||||
"url": "/MediaCover/86/banner.jpg?lastWrite=638509998968281535",
|
||||
"remoteUrl": "https://artworks.thetvdb.com/banners/graphical/5ccd960cc3aa0.jpg"
|
||||
},
|
||||
{
|
||||
"coverType": "poster",
|
||||
"url": "/MediaCover/86/poster.jpg?lastWrite=638509998969561521",
|
||||
"remoteUrl": "https://artworks.thetvdb.com/banners/v4/series/348545/posters/60908d475f49a.jpg"
|
||||
},
|
||||
{
|
||||
"coverType": "fanart",
|
||||
"url": "/MediaCover/86/fanart.jpg?lastWrite=638509998971801497",
|
||||
"remoteUrl": "https://artworks.thetvdb.com/banners/fanart/original/5c93cbb2b60b6.jpg"
|
||||
},
|
||||
{
|
||||
"coverType": "clearlogo",
|
||||
"url": "/MediaCover/86/clearlogo.png?lastWrite=638509998972921485",
|
||||
"remoteUrl": "https://artworks.thetvdb.com/banners/v4/series/348545/clearlogo/611c7fa8222d6.png"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"anime"
|
||||
]
|
||||
},
|
||||
"episodes": [
|
||||
{
|
||||
"id": 2716,
|
||||
"episodeNumber": 2,
|
||||
"seasonNumber": 5,
|
||||
"title": "Water Hashira Giyu Tomioka's Pain",
|
||||
"overview": "Kagaya's Kasugai Crow suddenly appears in front of Tamayo and invites her to the Demon Slayer headquarters — even though she is a demon. Meanwhile, Tanjiro, who is recovering at the Butterfly Mansion, receives a letter from Kagaya...",
|
||||
"airDate": "2024-05-19",
|
||||
"airDateUtc": "2024-05-19T14:15:00Z",
|
||||
"seriesId": 86,
|
||||
"tvdbId": 10445764
|
||||
}
|
||||
],
|
||||
"downloadInfo": {
|
||||
"quality": "WEBDL-1080p",
|
||||
"qualityVersion": 1,
|
||||
"title": "Demon.Slayer.Kimetsu.no.Yaiba.S57E02.Water.Hashira.Giyu.Tomiokas.Pain.1080p.CR.WEB-DL.JPN.AAC2.0.H.264.MSubs-ToonsHub.mkv",
|
||||
"size": 1446462408
|
||||
},
|
||||
"downloadClient": "aria2",
|
||||
"downloadClientType": "Aria2",
|
||||
"downloadId": "915718C3A8A5B15BD3C32A2B05885953D96AFADD",
|
||||
"downloadStatus": "Warning",
|
||||
"downloadStatusMessages": [
|
||||
{
|
||||
"title": "One or more episodes expected in this release were not imported or missing from the release",
|
||||
"messages": []
|
||||
},
|
||||
{
|
||||
"title": "Demon.Slayer.Kimetsu.no.Yaiba.S57E02.Water.Hashira.Giyu.Tomiokas.Pain.1080p.CR.WEB-DL.JPN.AAC2.0.H.264.MSubs-ToonsHub.mkv",
|
||||
"messages": [
|
||||
"Invalid season or episode"
|
||||
]
|
||||
}
|
||||
],
|
||||
"customFormatInfo": {
|
||||
"customFormats": [],
|
||||
"customFormatScore": 0
|
||||
},
|
||||
"release": {
|
||||
"releaseTitle": "[ToonsHub] Demon Slayer Kimetsu no Yaiba S05E02 1080p CR WEB-DL AAC2.0 x264 (Multi-Subs)",
|
||||
"indexer": "Knaben ",
|
||||
"size": 1395864320
|
||||
},
|
||||
"eventType": "ManualInteractionRequired",
|
||||
"instanceName": "Sonarr",
|
||||
"applicationUrl": ""
|
||||
}
|
||||
*/
|
||||
|
||||
var jsonObj = JsonConvert.DeserializeObject<JObject>(text);
|
||||
|
||||
var notify = @$"# Sonarr通知:
|
||||
@@ -302,7 +81,12 @@ public class NotifyController : BaseController
|
||||
|
||||
if (jsonObj["release"] != null)
|
||||
{
|
||||
var gb = (jsonObj["release"]!.ToObject<decimal>() / 1024M / 1024M / 1024M).ToString("0.##");
|
||||
var gb = jsonObj["release"]!["size"]!.ToString();
|
||||
|
||||
gb = decimal.TryParse(gb, out var size)
|
||||
? (size / 1024M / 1024M / 1024M).ToString("0.##")
|
||||
: "0";
|
||||
|
||||
notify += @$"
|
||||
索引器:<font color='info'> {jsonObj["release"]!["indexer"]} </font>
|
||||
发布组:<font color='info'> {jsonObj["release"]!["releaseGroup"]}({jsonObj["release"]!["quality"] ?? jsonObj["downloadInfo"]!["quality"]}) </font>
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
using Core;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace WebApi.Controllers;
|
||||
|
||||
public class XiaoController : BaseController
|
||||
{
|
||||
private readonly ILogger<XiaoController> _logger;
|
||||
|
||||
public XiaoController(ILogger<XiaoController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<string> GotoHomeGame()
|
||||
{
|
||||
await WxNotify.SendCommonAsync("接收到启动游戏机指令");
|
||||
|
||||
var commands = new[]
|
||||
{
|
||||
"grub-reboot 2",
|
||||
"reboot"
|
||||
};
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(3000);
|
||||
_ = await Command.ExecAsync(commands);
|
||||
});
|
||||
|
||||
return "OK";
|
||||
}
|
||||
}
|
||||
@@ -36,18 +36,18 @@ app.MapControllers();
|
||||
// JobManager.Initialize((DiskActionMonitorRegistry)diskActionMonitorRegistry);
|
||||
// var syncTaskRegistry = app.Services.GetRequiredService<IRSyncTaskRegistry>();
|
||||
// JobManager.Initialize((RSyncTaskRegistry)syncTaskRegistry);
|
||||
var healthyTaskRegistry = app.Services.GetRequiredService<IHealthyTaskRegistry>();
|
||||
JobManager.Initialize((HealthyTaskRegistry)healthyTaskRegistry);
|
||||
// var healthyTaskRegistry = app.Services.GetRequiredService<IHealthyTaskRegistry>();
|
||||
// JobManager.Initialize((HealthyTaskRegistry)healthyTaskRegistry);
|
||||
// var rsyncTaskRegistry = app.Services.GetRequiredService<IRSyncTaskRegistry>();
|
||||
// JobManager.Initialize((RSyncTaskRegistry)rsyncTaskRegistry);
|
||||
// var startupRegistry = app.Services.GetRequiredService<IStartupRegistry>();
|
||||
// JobManager.Initialize((StartupRegistry)startupRegistry);
|
||||
// var shutdownRegistry = app.Services.GetRequiredService<IShutdownRegistry>();
|
||||
// JobManager.Initialize((ShutdownRegistry)shutdownRegistry);
|
||||
var chineseNfoRegistry = app.Services.GetRequiredService<IChineseNfoRegistry>();
|
||||
JobManager.Initialize((ChineseNfoRegistry)chineseNfoRegistry);
|
||||
var diskMonitorRegistry = app.Services.GetRequiredService<IDiskMonitorRegistry>();
|
||||
JobManager.Initialize((DiskMonitorRegistry)diskMonitorRegistry);
|
||||
// var chineseNfoRegistry = app.Services.GetRequiredService<IChineseNfoRegistry>();
|
||||
// JobManager.Initialize((ChineseNfoRegistry)chineseNfoRegistry);
|
||||
// var diskMonitorRegistry = app.Services.GetRequiredService<IDiskMonitorRegistry>();
|
||||
// JobManager.Initialize((DiskMonitorRegistry)diskMonitorRegistry);
|
||||
#endif
|
||||
|
||||
app.Run();
|
||||
@@ -11,6 +11,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<PackageReference Include="taglib-sharp-netstandard2.0" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
],
|
||||
"ChineseNfo": {
|
||||
"TvFolder": "D:\\codes\\others\\ConvertChineseNfo",
|
||||
"HttpProxy": "http://suncheng.online:47890"
|
||||
"HttpProxy": "",
|
||||
"ConnectionString": "Data Source=D:\\codes\\others\\ConvertChineseNfo\\chinesenfo.db;"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user