Files
FATrace/FATrace.OEMApp/Services/JellyfinMonitorService.cs

244 lines
9.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FATrace.Com;
using FATrace.Model;
using NLog;
using FATrace.OEMApp;
namespace FATrace.OEMApp.Services
{
/// <summary>
/// 负责在 Jellyfin 中轮询查找新导入的视频,并在找到后更新数据库
/// </summary>
public sealed class JellyfinMonitorService
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly TimeSpan _pollInterval;
private readonly TimeSpan _maxWait;
public JellyfinMonitorService(TimeSpan? pollInterval = null, TimeSpan? maxWait = null)
{
// 支持从配置读取,也支持构造时传入覆盖
var cfgPoll = ConfigHelper.GetIntOrDefault("JellyfinPollIntervalMs", 5000);
var cfgMaxWait = ConfigHelper.GetIntOrDefault("JellyfinPollMaxWaitMs", 10 * 60 * 1000); // 10分钟
_pollInterval = pollInterval ?? TimeSpan.FromMilliseconds(cfgPoll);
_maxWait = maxWait ?? TimeSpan.FromMilliseconds(cfgMaxWait);
}
/// <summary>
/// 根据下载完成的本地文件名(或路径)轮询 Jellyfin找到后构建播放 URL并更新 OEMRawUse 与其一对一的 VideoAction。
/// </summary>
/// <param name="oemRawUseId">需要更新的 OEMRawUse Id</param>
/// <param name="videoLocalPathOrName">本地视频文件路径或文件名</param>
/// <param name="code">业务条码(用于 VideoAction.Code</param>
/// <param name="rawName">原料名称(业务字段)</param>
/// <param name="userName">用户名称</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task MonitorAndUpdateAfterDownloadAsync(
long oemRawUseId,
string videoLocalPathOrName,
string? code,
string? rawName,
string? userName,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(videoLocalPathOrName))
{
Logger.Warn("[JellyfinMonitor] videoLocalPathOrName 为空,取消轮询。");
return;
}
// 读取 Jellyfin 配置,并进行健壮性校验
string? baseUrl = null, apiKey = null, parentId = null;
try { baseUrl = ConfigHelper.GetStringOrDefault("JellyfinBaseUrl", string.Empty); } catch { }
try { apiKey = ConfigHelper.GetStringOrDefault("JellyfinApiKey", string.Empty); } catch { }
try { parentId = ConfigHelper.GetStringOrDefault("JellyfinParentId", string.Empty); } catch { }
if (string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(parentId))
{
Logger.Warn("[JellyfinMonitor] 缺少必要配置,跳过轮询: BaseUrl={Base}, ApiKeySet={ApiKeySet}, ParentIdSet={Parent}",
baseUrl, !string.IsNullOrWhiteSpace(apiKey), !string.IsNullOrWhiteSpace(parentId));
return;
}
var jfClient = new JellyfinClient(new JellyfinClientOptions
{
BaseUrl = baseUrl,
ApiKey = apiKey,
UseQueryApiKey = true,
TimeoutMs = 10000,
UserAgent = "FATrace.OEMApp/1.0"
});
// 准备待匹配的名称候选
var localName = Path.GetFileNameWithoutExtension(videoLocalPathOrName);
var candidates = BuildNameCandidates(localName);
var deadline = DateTime.UtcNow + _maxWait;
Logger.Info("[JellyfinMonitor] 开始轮询,等待 Jellyfin 索引文件: {LocalName}", localName);
string? foundId = null;
string? foundName = null;
while (DateTime.UtcNow < deadline && !cancellationToken.IsCancellationRequested)
{
try
{
var items = await jfClient.GetItemsOnlyAsync(parentId, sortBy: "DateCreated", sortOrder: "Descending", limit: 30, includeItemTypes: "Video,Movie", cancellationToken).ConfigureAwait(false);
var match = items.FirstOrDefault(it => !string.IsNullOrWhiteSpace(it.Name) && NameMatches(candidates, it.Name));
if (match != null && !string.IsNullOrWhiteSpace(match.Id))
{
foundId = match.Id;
foundName = match.Name;
Logger.Info("[JellyfinMonitor] 找到匹配视频: {Name}, Id={Id}", foundName, foundId);
break;
}
}
catch (Exception ex)
{
Logger.Warn(ex, "[JellyfinMonitor] 轮询 Jellyfin 失败,将在 {Delay} 后重试", _pollInterval);
}
await Task.Delay(_pollInterval, cancellationToken).ConfigureAwait(false);
}
if (string.IsNullOrWhiteSpace(foundId))
{
Logger.Warn("[JellyfinMonitor] 超时未找到匹配视频: {LocalName}", localName);
return;
}
// 构建播放 URL使用 query 传递 api_key
var playUrl = BuildStreamUrl(baseUrl, foundId, apiKey);
// 插入或更新 VideoAction并更新 OEMRawUse 绑定与 URL 字段(使用 ADO 事务,兼容当前 FreeSql 版本)
var db = FSqlContext.FDb;
var ado = db.Ado;
// 先获取目标 RawUse
var oem = await db.Select<OEMRawUse>().Where(x => x.Id == oemRawUseId).FirstAsync().ConfigureAwait(false);
if (oem == null)
{
Logger.Warn("[JellyfinMonitor] 未找到 OEMRawUse 记录Id={Id}", oemRawUseId);
return;
}
// 创建 VideoAction 记录
var action = new VideoAction
{
Code = code,
User = userName,
VideoFilePath = GetLocalPathIfExists(videoLocalPathOrName),
VideoName = foundName ?? localName,
StartTime = DateTime.Now,
EndTime = DateTime.Now,
CreateTime = DateTime.Now
};
try
{
db.Ado.Transaction(() =>
{
var actionId = (long)db.Insert<VideoAction>(action).ExecuteIdentity();
// 更新 OEMRawUse
db.Update<OEMRawUse>()
.Set(a => new OEMRawUse
{
UrlState = true,
VideoUrl = playUrl,
VideoActionId = actionId,
RawName = rawName
})
.Where(a => a.Id == oemRawUseId)
.ExecuteAffrows();
});
}
catch (Exception ex)
{
Logger.Error(ex, "[JellyfinMonitor] 更新数据库失败(插入 VideoAction 或更新 OEMRawUse");
return;
}
Logger.Info("[JellyfinMonitor] 已更新 OEMRawUse(Id={Id})UrlState=true, VideoUrl={Url}", oemRawUseId, playUrl);
}
private static string? GetLocalPathIfExists(string input)
{
try
{
if (string.IsNullOrWhiteSpace(input)) return null;
if (File.Exists(input)) return input;
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, input);
return File.Exists(path) ? path : input; // 若无法确认存在,原样存储
}
catch
{
return input;
}
}
private static string BuildStreamUrl(string baseUrl, string itemId, string apiKey)
{
baseUrl = baseUrl?.TrimEnd('/') ?? string.Empty;
var sb = new StringBuilder();
sb.Append(baseUrl)
.Append("/Videos/")
.Append(itemId)
.Append("/stream.mp4")
.Append("?api_key=")
.Append(Uri.EscapeDataString(apiKey));
return sb.ToString();
}
private static HashSet<string> BuildNameCandidates(string? name)
{
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(name)) return set;
var n = name.Trim();
set.Add(n);
set.Add(n.Replace('_', ' '));
set.Add(n.Replace(' ', '_'));
set.Add(RemoveNonAlphaNumeric(n));
set.Add(RemoveNonAlphaNumeric(n.Replace('_', ' ')));
set.Add(RemoveNonAlphaNumeric(n.Replace(' ', '_')));
return set;
}
private static bool NameMatches(HashSet<string> candidates, string? remoteName)
{
if (string.IsNullOrWhiteSpace(remoteName)) return false;
if (candidates.Contains(remoteName)) return true;
var normalized = RemoveNonAlphaNumeric(remoteName);
if (candidates.Contains(normalized)) return true;
// 近似匹配:任意候选作为子串
foreach (var c in candidates)
{
if (string.IsNullOrEmpty(c)) continue;
if (remoteName.IndexOf(c, StringComparison.OrdinalIgnoreCase) >= 0) return true;
if (normalized.IndexOf(RemoveNonAlphaNumeric(c), StringComparison.OrdinalIgnoreCase) >= 0) return true;
}
return false;
}
private static string RemoveNonAlphaNumeric(string s)
{
var sb = new StringBuilder(s.Length);
foreach (var ch in s)
{
if (char.IsLetterOrDigit(ch)) sb.Append(char.ToUpperInvariant(ch));
}
return sb.ToString();
}
}
}