feat: add fnOS packaging, storage workflows and release pipeline
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace dy.net.utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the a_bogus value used by Douyin's web search endpoint.
|
||||
/// The implementation is self-contained so fnOS packages do not need Node or a browser runtime.
|
||||
/// </summary>
|
||||
public sealed class DouyinABogusSigner
|
||||
{
|
||||
private const string UserAgent = DouyinRequestParamManager.DY_USER_AGENT;
|
||||
private const string Browser = "1536|742|1536|864|0|0|0|0|1536|864|1536|864|1536|742|24|24|Win32";
|
||||
private const string AlphabetS3 = "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe";
|
||||
private const string AlphabetS4 = "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe";
|
||||
|
||||
private static readonly uint[] InitialRegisters =
|
||||
{
|
||||
1937774191, 1226093241, 388252375, 3666478592,
|
||||
2842636476, 372324522, 3817729613, 2969243214
|
||||
};
|
||||
|
||||
private readonly byte[] _userAgentCode;
|
||||
private readonly int[] _browserCode = Browser.Select(x => (int)x).ToArray();
|
||||
|
||||
public DouyinABogusSigner()
|
||||
{
|
||||
var encrypted = Rc4Encrypt(UserAgent, "\0\x01\x0e");
|
||||
_userAgentCode = Sm3Hash(Encoding.UTF8.GetBytes(GenerateResult(encrypted, AlphabetS3)));
|
||||
}
|
||||
|
||||
public string Sign(string queryString)
|
||||
{
|
||||
var start = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var end = start + Random.Shared.Next(4, 9);
|
||||
return Sign(queryString, "GET", start, end,
|
||||
Random.Shared.NextDouble() * 10000,
|
||||
Random.Shared.NextDouble() * 10000,
|
||||
Random.Shared.NextDouble() * 10000);
|
||||
}
|
||||
|
||||
/// <summary>Deterministic overload used by protocol fixture tests.</summary>
|
||||
public string Sign(
|
||||
string queryString,
|
||||
string method,
|
||||
long startTimeMilliseconds,
|
||||
long endTimeMilliseconds,
|
||||
double randomNumber1,
|
||||
double randomNumber2,
|
||||
double randomNumber3)
|
||||
{
|
||||
if (queryString == null) throw new ArgumentNullException(nameof(queryString));
|
||||
var prefix = FromCharCodes(RandomList(randomNumber1, 170, 85, 1, 2, 5, 40))
|
||||
+ FromCharCodes(RandomList(randomNumber2, 170, 85, 1, 0, 0, 0))
|
||||
+ FromCharCodes(RandomList(randomNumber3, 170, 85, 1, 0, 5, 0));
|
||||
var payload = GeneratePayload(queryString, method, startTimeMilliseconds, endTimeMilliseconds);
|
||||
return GenerateResult(prefix + payload, AlphabetS4);
|
||||
}
|
||||
|
||||
private string GeneratePayload(string queryString, string method, long start, long end)
|
||||
{
|
||||
var parameterCode = Sm3Hash(Sm3Hash(Encoding.UTF8.GetBytes(queryString + "cus")));
|
||||
var methodCode = Sm3Hash(Sm3Hash(Encoding.UTF8.GetBytes((method ?? "GET") + "cus")));
|
||||
var values = BuildPayloadValues(
|
||||
(int)((end >> 24) & 255), parameterCode[21], _userAgentCode[23],
|
||||
(int)((end >> 16) & 255), parameterCode[22], _userAgentCode[24],
|
||||
(int)((end >> 8) & 255), (int)(end & 255),
|
||||
(int)((start >> 24) & 255), (int)((start >> 16) & 255),
|
||||
(int)((start >> 8) & 255), (int)(start & 255),
|
||||
methodCode[21], methodCode[22],
|
||||
(int)(end / 4294967296d), (int)(start / 4294967296d), Browser.Length);
|
||||
var check = values.Aggregate(0, (current, value) => current ^ value);
|
||||
values.AddRange(_browserCode);
|
||||
values.Add(check);
|
||||
return Rc4Encrypt(FromCharCodes(values), "y");
|
||||
}
|
||||
|
||||
private static List<int> BuildPayloadValues(
|
||||
int a, int b, int c, int d, int e, int f, int g, int h,
|
||||
int i, int j, int k, int m, int n, int o, int p, int q, int r) =>
|
||||
new()
|
||||
{
|
||||
44, a, 0, 0, 0, 0, 24, b, n, 0, c, d, 0, 0, 0, 1, 0, 239,
|
||||
e, o, f, g, 0, 0, 0, 0, h, 0, 0, 14, i, j, 0, k, m, 3, p, 1,
|
||||
q, 1, r, 0, 0, 0
|
||||
};
|
||||
|
||||
private static IEnumerable<int> RandomList(
|
||||
double randomNumber, int evenMask, int oddMask, int evenLow, int oddLow, int evenHigh, int oddHigh)
|
||||
{
|
||||
var value = (int)randomNumber;
|
||||
var low = value & 255;
|
||||
var high = value >> 8;
|
||||
return new[]
|
||||
{
|
||||
(low & evenMask) | evenLow,
|
||||
(low & oddMask) | oddLow,
|
||||
(high & evenMask) | evenHigh,
|
||||
(high & oddMask) | oddHigh
|
||||
};
|
||||
}
|
||||
|
||||
private static string FromCharCodes(IEnumerable<int> values) =>
|
||||
new(values.Select(value => (char)value).ToArray());
|
||||
|
||||
private static string Rc4Encrypt(string plaintext, string key)
|
||||
{
|
||||
var state = Enumerable.Range(0, 256).ToArray();
|
||||
var j = 0;
|
||||
for (var i = 0; i < 256; i++)
|
||||
{
|
||||
j = (j + state[i] + key[i % key.Length]) % 256;
|
||||
(state[i], state[j]) = (state[j], state[i]);
|
||||
}
|
||||
|
||||
var output = new char[plaintext.Length];
|
||||
var x = 0;
|
||||
j = 0;
|
||||
for (var index = 0; index < plaintext.Length; index++)
|
||||
{
|
||||
x = (x + 1) % 256;
|
||||
j = (j + state[x]) % 256;
|
||||
(state[x], state[j]) = (state[j], state[x]);
|
||||
var keyByte = state[(state[x] + state[j]) % 256];
|
||||
output[index] = (char)(keyByte ^ plaintext[index]);
|
||||
}
|
||||
return new string(output);
|
||||
}
|
||||
|
||||
private static string GenerateResult(string value, string alphabet)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
for (var index = 0; index < value.Length; index += 3)
|
||||
{
|
||||
var number = value[index] << 16;
|
||||
if (index + 1 < value.Length) number |= value[index + 1] << 8;
|
||||
if (index + 2 < value.Length) number |= value[index + 2];
|
||||
|
||||
result.Append(alphabet[(number & 0xfc0000) >> 18]);
|
||||
result.Append(alphabet[(number & 0x03f000) >> 12]);
|
||||
if (index + 1 < value.Length) result.Append(alphabet[(number & 0x0fc0) >> 6]);
|
||||
if (index + 2 < value.Length) result.Append(alphabet[number & 0x3f]);
|
||||
}
|
||||
while (result.Length % 4 != 0) result.Append('=');
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private static byte[] Sm3Hash(byte[] data)
|
||||
{
|
||||
var bitLength = (ulong)data.LongLength * 8;
|
||||
var paddedLength = ((data.Length + 1 + 8 + 63) / 64) * 64;
|
||||
var padded = new byte[paddedLength];
|
||||
Buffer.BlockCopy(data, 0, padded, 0, data.Length);
|
||||
padded[data.Length] = 0x80;
|
||||
BinaryPrimitives.WriteUInt64BigEndian(padded.AsSpan(padded.Length - 8), bitLength);
|
||||
|
||||
var registers = (uint[])InitialRegisters.Clone();
|
||||
for (var offset = 0; offset < padded.Length; offset += 64)
|
||||
Compress(registers, padded.AsSpan(offset, 64));
|
||||
|
||||
var result = new byte[32];
|
||||
for (var index = 0; index < registers.Length; index++)
|
||||
BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan(index * 4, 4), registers[index]);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void Compress(uint[] registers, ReadOnlySpan<byte> block)
|
||||
{
|
||||
var words = new uint[68];
|
||||
var expanded = new uint[64];
|
||||
for (var index = 0; index < 16; index++)
|
||||
words[index] = BinaryPrimitives.ReadUInt32BigEndian(block.Slice(index * 4, 4));
|
||||
for (var index = 16; index < 68; index++)
|
||||
{
|
||||
var value = words[index - 16] ^ words[index - 9] ^ RotateLeft(words[index - 3], 15);
|
||||
words[index] = P1(value) ^ RotateLeft(words[index - 13], 7) ^ words[index - 6];
|
||||
}
|
||||
for (var index = 0; index < 64; index++) expanded[index] = words[index] ^ words[index + 4];
|
||||
|
||||
var a = registers[0];
|
||||
var b = registers[1];
|
||||
var c = registers[2];
|
||||
var d = registers[3];
|
||||
var e = registers[4];
|
||||
var f = registers[5];
|
||||
var g = registers[6];
|
||||
var h = registers[7];
|
||||
|
||||
for (var index = 0; index < 64; index++)
|
||||
{
|
||||
var rotatedA = RotateLeft(a, 12);
|
||||
var ss1 = RotateLeft(unchecked(rotatedA + e + RotateLeft(index < 16 ? 0x79cc4519u : 0x7a879d8au, index)), 7);
|
||||
var ss2 = ss1 ^ rotatedA;
|
||||
var tt1 = unchecked(Ff(index, a, b, c) + d + ss2 + expanded[index]);
|
||||
var tt2 = unchecked(Gg(index, e, f, g) + h + ss1 + words[index]);
|
||||
d = c;
|
||||
c = RotateLeft(b, 9);
|
||||
b = a;
|
||||
a = tt1;
|
||||
h = g;
|
||||
g = RotateLeft(f, 19);
|
||||
f = e;
|
||||
e = P0(tt2);
|
||||
}
|
||||
|
||||
registers[0] ^= a;
|
||||
registers[1] ^= b;
|
||||
registers[2] ^= c;
|
||||
registers[3] ^= d;
|
||||
registers[4] ^= e;
|
||||
registers[5] ^= f;
|
||||
registers[6] ^= g;
|
||||
registers[7] ^= h;
|
||||
}
|
||||
|
||||
private static uint RotateLeft(uint value, int count)
|
||||
{
|
||||
count &= 31;
|
||||
return count == 0 ? value : (value << count) | (value >> (32 - count));
|
||||
}
|
||||
|
||||
private static uint P0(uint value) => value ^ RotateLeft(value, 9) ^ RotateLeft(value, 17);
|
||||
|
||||
private static uint P1(uint value) => value ^ RotateLeft(value, 15) ^ RotateLeft(value, 23);
|
||||
|
||||
private static uint Ff(int index, uint x, uint y, uint z) =>
|
||||
index < 16 ? x ^ y ^ z : (x & y) | (x & z) | (y & z);
|
||||
|
||||
private static uint Gg(int index, uint x, uint y, uint z) =>
|
||||
index < 16 ? x ^ y ^ z : (x & y) | (~x & z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using dy.net.model.dto;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace dy.net.utils
|
||||
{
|
||||
public static class DouyinLiveStatusParser
|
||||
{
|
||||
public static DouyinLiveStatusProbe Parse(
|
||||
int? liveStatus,
|
||||
string roomId,
|
||||
string roomIdStr,
|
||||
JToken roomData)
|
||||
{
|
||||
var room = NormalizeRoomData(roomData);
|
||||
var roomStatus = FindInt(room, "status");
|
||||
var isLive = liveStatus == 1 || (!liveStatus.HasValue && roomStatus == 2);
|
||||
var hasKnownStatus = liveStatus.HasValue || roomStatus.HasValue;
|
||||
|
||||
return new DouyinLiveStatusProbe
|
||||
{
|
||||
Status = isLive
|
||||
? DouyinLiveStatusState.Live
|
||||
: hasKnownStatus ? DouyinLiveStatusState.Offline : DouyinLiveStatusState.Unknown,
|
||||
RoomId = FirstNonEmpty(
|
||||
roomIdStr,
|
||||
roomId,
|
||||
FindString(room, "id_str"),
|
||||
FindString(room, "room_id_str"),
|
||||
FindString(room, "room_id")),
|
||||
WebRid = FirstNonEmpty(
|
||||
FindString(room, "web_rid"),
|
||||
FindString(room, "webRid")),
|
||||
Title = Limit(FindString(room, "title"), 500),
|
||||
StartedAt = ParseUnixTime(FindLong(room, "start_time") ?? FindLong(room, "create_time"))
|
||||
};
|
||||
}
|
||||
|
||||
private static JToken NormalizeRoomData(JToken value)
|
||||
{
|
||||
if (value == null || value.Type == JTokenType.Null) return null;
|
||||
if (value.Type != JTokenType.String) return value;
|
||||
var text = value.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(text)) return null;
|
||||
try { return JToken.Parse(text); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static string FindString(JToken token, string name)
|
||||
{
|
||||
var value = Find(token, name);
|
||||
return value?.Type == JTokenType.Null ? null : value?.ToString();
|
||||
}
|
||||
|
||||
private static int? FindInt(JToken token, string name)
|
||||
{
|
||||
var value = Find(token, name);
|
||||
return int.TryParse(value?.ToString(), out var parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
private static long? FindLong(JToken token, string name)
|
||||
{
|
||||
var value = Find(token, name);
|
||||
return long.TryParse(value?.ToString(), out var parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
private static JToken Find(JToken token, string name)
|
||||
{
|
||||
if (token == null) return null;
|
||||
if (token is JObject obj)
|
||||
{
|
||||
var property = obj.Properties().FirstOrDefault(x =>
|
||||
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
if (property != null) return property.Value;
|
||||
foreach (var child in obj.Properties())
|
||||
{
|
||||
var found = Find(child.Value, name);
|
||||
if (found != null) return found;
|
||||
}
|
||||
}
|
||||
else if (token is JArray array)
|
||||
{
|
||||
foreach (var child in array)
|
||||
{
|
||||
var found = Find(child, name);
|
||||
if (found != null) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string FirstNonEmpty(params string[] values) =>
|
||||
values.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x))?.Trim();
|
||||
|
||||
private static DateTime? ParseUnixTime(long? value)
|
||||
{
|
||||
if (!value.HasValue || value <= 0) return null;
|
||||
try
|
||||
{
|
||||
var timestamp = value > 10_000_000_000
|
||||
? DateTimeOffset.FromUnixTimeMilliseconds(value.Value)
|
||||
: DateTimeOffset.FromUnixTimeSeconds(value.Value);
|
||||
return timestamp.LocalDateTime;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static string Limit(string value, int maxLength) =>
|
||||
string.IsNullOrWhiteSpace(value)
|
||||
? null
|
||||
: value.Trim().Length <= maxLength ? value.Trim() : value.Trim()[..maxLength];
|
||||
}
|
||||
}
|
||||
+65
-49
@@ -236,12 +236,11 @@ namespace dy.net.utils
|
||||
if (imageFilePaths == null || !imageFilePaths.Any())
|
||||
throw new ArgumentException("图片路径列表不能为空。", nameof(imageFilePaths));
|
||||
|
||||
if (string.IsNullOrEmpty(audioFilePath) || !File.Exists(audioFilePath))
|
||||
throw new FileNotFoundException("音频文件未找到。", audioFilePath);
|
||||
|
||||
if (string.IsNullOrEmpty(outputVideoPath))
|
||||
throw new ArgumentNullException(nameof(outputVideoPath));
|
||||
|
||||
var hasAudio = !string.IsNullOrWhiteSpace(audioFilePath) && File.Exists(audioFilePath);
|
||||
|
||||
foreach (var imagePath in imageFilePaths)
|
||||
{
|
||||
if (!File.Exists(imagePath.Path))
|
||||
@@ -278,7 +277,7 @@ namespace dy.net.utils
|
||||
|
||||
// 获取音频时长
|
||||
double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds;
|
||||
try
|
||||
if (hasAudio) try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
@@ -329,50 +328,10 @@ namespace dy.net.utils
|
||||
// 组合最终滤镜:先适配尺寸,再循环
|
||||
string filterComplex = $"[0:v]{fitFilter},loop=loop={loopCount - 1}:size={imageCount}[v]";
|
||||
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"-y", // 覆盖输出文件
|
||||
|
||||
// 图片序列输入
|
||||
"-f", "image2",
|
||||
"-framerate", imageFps.ToString(CultureInfo.InvariantCulture),
|
||||
"-start_number", "1", // 修正:临时文件是 temp_001、temp_002... 所以起始编号为1
|
||||
"-i", imageSequencePattern,
|
||||
|
||||
// 音频输入
|
||||
"-i", audioFilePath,
|
||||
|
||||
// 滤镜:尺寸适配 + 循环
|
||||
"-filter_complex", filterComplex,
|
||||
|
||||
// 流映射
|
||||
"-map", "[v]",
|
||||
"-map", "1:a",
|
||||
|
||||
// 视频编码参数
|
||||
"-c:v", VideoCodec,
|
||||
"-preset", VideoPreset,
|
||||
"-crf", $"{VideoCrf}",
|
||||
"-s", $"{VideoWidth}x{VideoHeight}",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-profile:v", "main",
|
||||
|
||||
// 音频编码参数
|
||||
"-c:a", AudioCodec,
|
||||
"-b:a", $"{AudioBitrate}",
|
||||
"-ac", "2",
|
||||
"-ar", "44100",
|
||||
|
||||
// 封装优化
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart",
|
||||
|
||||
// 同步参数
|
||||
"-shortest",
|
||||
|
||||
// 输出路径
|
||||
outputVideoPath
|
||||
};
|
||||
var arguments = BuildImageVideoArguments(
|
||||
imageSequencePattern, hasAudio ? audioFilePath : null, outputVideoPath,
|
||||
imageFps, filterComplex, VideoWidth, VideoHeight,
|
||||
VideoCodec, VideoPreset, VideoCrf, AudioCodec, AudioBitrate);
|
||||
|
||||
// 执行FFmpeg命令
|
||||
await ExecuteFFmpegAsync(arguments, progress, cancellationToken);
|
||||
@@ -401,6 +360,63 @@ namespace dy.net.utils
|
||||
}
|
||||
}
|
||||
|
||||
internal static List<string> BuildImageVideoArguments(
|
||||
string imageSequencePattern,
|
||||
string audioFilePath,
|
||||
string outputVideoPath,
|
||||
double imageFps,
|
||||
string filterComplex,
|
||||
int videoWidth,
|
||||
int videoHeight,
|
||||
string videoCodec = "libx264",
|
||||
string videoPreset = "medium",
|
||||
int videoCrf = 23,
|
||||
string audioCodec = "aac",
|
||||
string audioBitrate = "192k")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imageSequencePattern)) throw new ArgumentNullException(nameof(imageSequencePattern));
|
||||
if (string.IsNullOrWhiteSpace(outputVideoPath)) throw new ArgumentNullException(nameof(outputVideoPath));
|
||||
if (string.IsNullOrWhiteSpace(filterComplex)) throw new ArgumentNullException(nameof(filterComplex));
|
||||
|
||||
var hasAudio = !string.IsNullOrWhiteSpace(audioFilePath) && File.Exists(audioFilePath);
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"-y",
|
||||
"-f", "image2",
|
||||
"-framerate", imageFps.ToString(CultureInfo.InvariantCulture),
|
||||
"-start_number", "1",
|
||||
"-i", imageSequencePattern
|
||||
};
|
||||
if (hasAudio) arguments.AddRange(new[] { "-i", audioFilePath });
|
||||
arguments.AddRange(new[]
|
||||
{
|
||||
"-filter_complex", filterComplex,
|
||||
"-map", "[v]",
|
||||
"-c:v", videoCodec,
|
||||
"-preset", videoPreset,
|
||||
"-crf", $"{videoCrf}",
|
||||
"-s", $"{videoWidth}x{videoHeight}",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-profile:v", "main"
|
||||
});
|
||||
if (hasAudio)
|
||||
{
|
||||
arguments.AddRange(new[]
|
||||
{
|
||||
"-map", "1:a",
|
||||
"-c:a", audioCodec,
|
||||
"-b:a", audioBitrate,
|
||||
"-ac", "2",
|
||||
"-ar", "44100"
|
||||
});
|
||||
}
|
||||
else arguments.Add("-an");
|
||||
arguments.AddRange(new[] { "-f", "mp4", "-movflags", "+faststart" });
|
||||
if (hasAudio) arguments.Add("-shortest");
|
||||
arguments.Add(outputVideoPath);
|
||||
return arguments;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -782,4 +798,4 @@ namespace dy.net.utils
|
||||
_ffmpegProcess?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Xml.Linq;
|
||||
using System.Xml;
|
||||
using dy.net.model.dto;
|
||||
using dy.net.model.entity;
|
||||
using dy.net.storage;
|
||||
|
||||
namespace dy.net.utils
|
||||
{
|
||||
public static class NfoContentBuilder
|
||||
{
|
||||
public static IReadOnlyDictionary<string, string> Build(DouyinVideo video, string tvShowTitle = "")
|
||||
{
|
||||
var files = new Dictionary<string, string>();
|
||||
if (video == null || video.OnlyImgOrOnlyMp3 || string.IsNullOrWhiteSpace(video.VideoSavePath)) return files;
|
||||
|
||||
var directory = StoragePath.DirectoryName(video.VideoSavePath);
|
||||
var fileName = Path.GetFileNameWithoutExtension(video.VideoSavePath);
|
||||
var isSeries = video.ViedoType == VideoTypeEnum.dy_mix || video.ViedoType == VideoTypeEnum.dy_series;
|
||||
|
||||
if (isSeries)
|
||||
{
|
||||
files[StoragePath.CombineRemote(directory, "tvshow.nfo")] = CreateXml(video, "tvshow", string.IsNullOrWhiteSpace(tvShowTitle) ? video.VideoTitle : tvShowTitle);
|
||||
files[StoragePath.CombineRemote(directory, fileName + ".nfo")] = CreateXml(video, "episodedetails", video.VideoTitle);
|
||||
}
|
||||
else
|
||||
{
|
||||
files[StoragePath.CombineRemote(directory, fileName + ".nfo")] = CreateXml(video, "movie", video.VideoTitle);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private static string CreateXml(DouyinVideo video, string rootName, string title)
|
||||
{
|
||||
var root = new XElement(rootName,
|
||||
new XElement("lockdata", true),
|
||||
new XElement("title", Clean(title)),
|
||||
new XElement("releasedate", video.CreateTime.ToString("yyyy-MM-dd")),
|
||||
new XElement("premiered", video.CreateTime.ToString("yyyy-MM-dd")));
|
||||
|
||||
foreach (var genre in new[] { video.Tag1, video.Tag2, video.Tag3 }.Where(x => !string.IsNullOrWhiteSpace(x)))
|
||||
root.Add(new XElement("genre", Clean(genre)));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(video.Author))
|
||||
{
|
||||
root.Add(new XElement("actor",
|
||||
new XElement("name", Clean(video.Author)),
|
||||
new XElement("role", "主演"),
|
||||
new XElement("tmdbid", "")));
|
||||
}
|
||||
|
||||
var poster = Path.GetFileName(video.VideoCoverSavePath);
|
||||
if (!string.IsNullOrWhiteSpace(poster))
|
||||
{
|
||||
root.Add(new XElement("thumb", new XAttribute("aspect", "poster"), poster));
|
||||
root.Add(new XElement("fanart", new XElement("thumb", poster)));
|
||||
}
|
||||
|
||||
if (rootName == "episodedetails")
|
||||
{
|
||||
root.Add(new XElement("episode", Path.GetFileNameWithoutExtension(video.VideoSavePath).Replace("S01E0", "").Replace("S01E", "")));
|
||||
root.Add(new XElement("season", "1"));
|
||||
}
|
||||
|
||||
return new XDocument(new XDeclaration("1.0", "utf-8", null), root).ToString();
|
||||
}
|
||||
|
||||
private static string Clean(string value) => string.IsNullOrWhiteSpace(value)
|
||||
? string.Empty
|
||||
: new string(value.Where(ch => XmlConvert.IsXmlChar(ch)).ToArray());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user