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 _logger; private readonly HttpClient _client; public ChineseNfoRegistry(IConfiguration configuration, ILogger 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("https://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解析器 读取 60059 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 = $""; 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 = $""; 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 = $"https://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) { // 删除原有的 节点 var genreNodes = tvXml.SelectNodes("//genre"); if (genreNodes != null) { foreach (XmlNode genreNode in genreNodes) { tvXml.DocumentElement?.RemoveChild(genreNode); } } // 添加新的 节点 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; } // 读取 1 var seasonNode = episodeXml.SelectSingleNode("//season"); if (seasonNode == null) { failedCount++; Console.WriteLine($"{episodeFile} & 未找到 season"); return; } // 读取 1 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 = $""; if (titleNode.InnerXml != title) { isUpdate = true; // 写入且不转义 titleNode.InnerXml = title; } } var plotNode = episodeXml.SelectSingleNode("//plot"); if (!string.IsNullOrEmpty(overview) && plotNode != null) { 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 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); 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; } } private async Task 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; } } }