1、去重
2、分享 3、视频播放 4、批量删除,重新下载 5、自定义标题(仅博主视频) 6、授权页面去掉博主添加,新增关注列表 7、图文视频合成优化 8、其他优化
This commit is contained in:
+1
-1
@@ -205,7 +205,7 @@ namespace dy.net.utils
|
||||
return Sm3ToArray(Sm3ToArray(method + _endString));
|
||||
}
|
||||
|
||||
// ... 其他成员和初始化代码 ...
|
||||
|
||||
|
||||
public int[] Sm3ToArray(string data)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,535 @@
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
using Org.BouncyCastle.Crypto.Engines;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using System.Text;
|
||||
|
||||
namespace dy.net.utils
|
||||
{
|
||||
public static class StringProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// 将字符串转换为字符数组 (ASCII)
|
||||
/// </summary>
|
||||
public static int[] ToOrdArray(string s)
|
||||
{
|
||||
return s.Select(c => (int)c).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将整数数组转回字符串
|
||||
/// </summary>
|
||||
public static string ToCharStr(int[] arr)
|
||||
{
|
||||
return new string(arr.Select(i => (char)i).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JavaScript 无符号右移操作 (>>>)
|
||||
/// </summary>
|
||||
public static int JsShiftRight(int value, int n)
|
||||
{
|
||||
uint uValue = (uint)value;
|
||||
return (int)(uValue >> n);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成伪随机混淆字节字符串 (长度为 length * 4)
|
||||
/// </summary>
|
||||
private static Random _random = new Random();
|
||||
public static string GenerateRandomBytes(int length = 3)
|
||||
{
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
int rd = _random.Next(10000);
|
||||
|
||||
result.Append((char)(((rd & 255) & 170) | 1));
|
||||
result.Append((char)(((rd & 255) & 85) | 2));
|
||||
result.Append((char)((JsShiftRight(rd, 8) & 170) | 5));
|
||||
result.Append((char)((JsShiftRight(rd, 8) & 85) | 40));
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class CryptoUtility
|
||||
{
|
||||
public string Salt { get; set; }
|
||||
public List<string> Base64Alphabet { get; set; }
|
||||
|
||||
private readonly int[] _bigArray = {
|
||||
121, 243, 55, 234, 103, 36, 47, 228, 30, 231, 106, 6, 115, 95, 78, 101,
|
||||
250, 207, 198, 50, 139, 227, 220, 105, 97, 143, 34, 28, 194, 215, 18, 100,
|
||||
159, 160, 43, 8, 169, 217, 180, 120, 247, 45, 90, 11, 27, 197, 46, 3,
|
||||
84, 72, 5, 68, 62, 56, 221, 75, 144, 79, 73, 161, 178, 81, 64, 187,
|
||||
134, 117, 186, 118, 16, 241, 130, 71, 89, 147, 122, 129, 65, 40, 88, 150,
|
||||
110, 219, 199, 255, 181, 254, 48, 4, 195, 248, 208, 32, 116, 167, 69, 201,
|
||||
17, 124, 125, 104, 96, 83, 80, 127, 236, 108, 154, 126, 204, 15, 20, 135,
|
||||
112, 158, 13, 1, 188, 164, 210, 237, 222, 98, 212, 77, 253, 42, 170, 202,
|
||||
26, 22, 29, 182, 251, 10, 173, 152, 58, 138, 54, 141, 185, 33, 157, 31,
|
||||
252, 132, 233, 235, 102, 196, 191, 223, 240, 148, 39, 123, 92, 82, 128, 109,
|
||||
57, 24, 38, 113, 209, 245, 2, 119, 153, 229, 189, 214, 230, 174, 232, 63,
|
||||
52, 205, 86, 140, 66, 175, 111, 171, 246, 133, 238, 193, 99, 60, 74, 91,
|
||||
225, 51, 76, 37, 145, 211, 166, 151, 213, 206, 0, 200, 244, 176, 218, 44,
|
||||
184, 172, 49, 216, 93, 168, 53, 21, 183, 41, 67, 85, 224, 155, 226, 242,
|
||||
87, 177, 146, 70, 190, 12, 162, 19, 137, 114, 25, 165, 163, 192, 23, 59,
|
||||
9, 94, 179, 107, 35, 7, 142, 131, 239, 203, 149, 136, 61, 249, 14, 156
|
||||
};
|
||||
|
||||
public CryptoUtility(string salt, List<string> base64Alphabet)
|
||||
{
|
||||
Salt = salt;
|
||||
Base64Alphabet = base64Alphabet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 SM3 哈希并返回 byte 数组(即整数列表)
|
||||
/// </summary>
|
||||
public static byte[] Sm3Hash(byte[] input)
|
||||
{
|
||||
var digest = new SM3Digest();
|
||||
digest.BlockUpdate(input, 0, input.Length);
|
||||
byte[] output = new byte[digest.GetDigestSize()];
|
||||
digest.DoFinal(output, 0);
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对输入数据计算 SM3 哈希,并返回整数数组
|
||||
/// </summary>
|
||||
public int[] Sm3ToArray(object input)
|
||||
{
|
||||
byte[] bytes;
|
||||
|
||||
if (input is string str)
|
||||
{
|
||||
bytes = Encoding.UTF8.GetBytes(str);
|
||||
}
|
||||
else if (input is int[] arr)
|
||||
{
|
||||
bytes = arr.Select(b => (byte)b).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Input must be string or int[]");
|
||||
}
|
||||
|
||||
byte[] hash = Sm3Hash(bytes);
|
||||
return hash.Select(b => (int)b).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加盐值
|
||||
/// </summary>
|
||||
public string AddSalt(string param)
|
||||
{
|
||||
return param + Salt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理参数(可选加盐)
|
||||
/// </summary>
|
||||
public object ProcessParam(object param, bool addSalt)
|
||||
{
|
||||
if (param is string s && addSalt)
|
||||
{
|
||||
return AddSalt(s);
|
||||
}
|
||||
return param;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取参数哈希数组(双重哈希)
|
||||
/// </summary>
|
||||
public int[] ParamsToArray(object param, bool addSalt = true)
|
||||
{
|
||||
var processed = ProcessParam(param, addSalt);
|
||||
var firstHash = Sm3ToArray(processed);
|
||||
return Sm3ToArray(firstHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RC4 加密
|
||||
/// </summary>
|
||||
public static byte[] Rc4Encrypt(byte[] key, string plaintext)
|
||||
{
|
||||
byte[] data = Encoding.UTF8.GetBytes(plaintext);
|
||||
var rc4 = new RC4Engine();
|
||||
rc4.Init(true, new KeyParameter(key));
|
||||
|
||||
byte[] output = new byte[data.Length];
|
||||
rc4.ProcessBytes(data, 0, data.Length, output, 0);
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 Base64 编码
|
||||
/// </summary>
|
||||
public string Base64Encode(string input, int selectedAlphabet = 0)
|
||||
{
|
||||
string alphabet = Base64Alphabet[selectedAlphabet];
|
||||
var binary = new StringBuilder();
|
||||
|
||||
foreach (char c in input)
|
||||
{
|
||||
binary.Append(Convert.ToString(c, 2).PadLeft(8, '0'));
|
||||
}
|
||||
|
||||
while (binary.Length % 6 != 0)
|
||||
{
|
||||
binary.Append('0');
|
||||
}
|
||||
|
||||
var chunks = new List<int>();
|
||||
for (int i = 0; i < binary.Length; i += 6)
|
||||
{
|
||||
string chunk = binary.ToString(i, Math.Min(6, binary.Length - i));
|
||||
chunks.Add(Convert.ToInt32(chunk, 2));
|
||||
}
|
||||
|
||||
var output = new StringBuilder();
|
||||
foreach (int index in chunks)
|
||||
{
|
||||
output.Append(alphabet[index]);
|
||||
}
|
||||
|
||||
// Padding
|
||||
int padding = (6 - (binary.Length % 6)) % 6;
|
||||
output.Append('=', padding / 2);
|
||||
|
||||
return output.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ABogus 自定义编码逻辑(类似Base64但不同分组)
|
||||
/// </summary>
|
||||
public string AbogusEncode(string input, int selectedAlphabet)
|
||||
{
|
||||
var abogus = new List<char>();
|
||||
string alphabet = Base64Alphabet[selectedAlphabet];
|
||||
|
||||
for (int i = 0; i < input.Length; i += 3)
|
||||
{
|
||||
int n = 0;
|
||||
if (i + 2 < input.Length)
|
||||
{
|
||||
n = (input[i] << 16) | (input[i + 1] << 8) | input[i + 2];
|
||||
}
|
||||
else if (i + 1 < input.Length)
|
||||
{
|
||||
n = (input[i] << 16) | (input[i + 1] << 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
n = input[i] << 16;
|
||||
}
|
||||
|
||||
int[] masks = { 0xFC0000, 0x03F000, 0x0FC0, 0x3F };
|
||||
int[] shifts = { 18, 12, 6, 0 };
|
||||
|
||||
for (int j = 0; j < 4; j++)
|
||||
{
|
||||
if ((j == 2 && i + 1 >= input.Length) || (j == 3 && i + 2 >= input.Length))
|
||||
break;
|
||||
int val = (n & masks[j]) >> shifts[j];
|
||||
abogus.Add(alphabet[val]);
|
||||
}
|
||||
}
|
||||
|
||||
while (abogus.Count % 4 != 0)
|
||||
{
|
||||
abogus.Add('=');
|
||||
}
|
||||
|
||||
return new string(abogus.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字节数组变换加密(RC4-like 流密码)
|
||||
/// </summary>
|
||||
public string TransformBytes(int[] bytesList)
|
||||
{
|
||||
string bytesStr = StringProcessor.ToCharStr(bytesList);
|
||||
var result = new List<char>();
|
||||
|
||||
int indexB = _bigArray[1];
|
||||
int initialValue = 0;
|
||||
int valueE = 0;
|
||||
|
||||
for (int i = 0; i < bytesStr.Length; i++)
|
||||
{
|
||||
char ch = bytesStr[i];
|
||||
int charValue = ch;
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
initialValue = _bigArray[indexB];
|
||||
int sumInitial = indexB + initialValue;
|
||||
|
||||
_bigArray[1] = initialValue;
|
||||
_bigArray[indexB] = indexB;
|
||||
}
|
||||
else
|
||||
{
|
||||
int sumInitial = initialValue + valueE;
|
||||
sumInitial %= _bigArray.Length;
|
||||
valueE = _bigArray[(i + 2) % _bigArray.Length];
|
||||
sumInitial = (indexB + valueE) % _bigArray.Length;
|
||||
initialValue = _bigArray[sumInitial];
|
||||
}
|
||||
|
||||
int sumInitialFinal = (indexB + (i == 0 ? initialValue : valueE)) % _bigArray.Length;
|
||||
int valueF = _bigArray[sumInitialFinal];
|
||||
int encryptedChar = charValue ^ valueF;
|
||||
result.Add((char)encryptedChar);
|
||||
|
||||
// 更新状态
|
||||
valueE = _bigArray[(i + 2) % _bigArray.Length];
|
||||
sumInitialFinal = (indexB + valueE) % _bigArray.Length;
|
||||
int temp = _bigArray[sumInitialFinal];
|
||||
_bigArray[sumInitialFinal] = _bigArray[(i + 2) % _bigArray.Length];
|
||||
_bigArray[(i + 2) % _bigArray.Length] = temp;
|
||||
indexB = sumInitialFinal;
|
||||
}
|
||||
|
||||
return new string(result.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public class BrowserFingerprintGenerator
|
||||
{
|
||||
private static Random _random = new Random();
|
||||
|
||||
public static string GenerateFingerprint(string browserType = "Edge")
|
||||
{
|
||||
return browserType switch
|
||||
{
|
||||
"Chrome" => _GenerateFingerprint("Win32"),
|
||||
"Firefox" => _GenerateFingerprint("Win32"),
|
||||
"Safari" => _GenerateFingerprint("MacIntel"),
|
||||
"Edge" => _GenerateFingerprint("Win32"),
|
||||
_ => _GenerateFingerprint("Win32")
|
||||
};
|
||||
}
|
||||
|
||||
private static string _GenerateFingerprint(string platform)
|
||||
{
|
||||
int innerWidth = _random.Next(1024, 1921);
|
||||
int innerHeight = _random.Next(768, 1081);
|
||||
int outerWidth = innerWidth + _random.Next(24, 33);
|
||||
int outerHeight = innerHeight + _random.Next(75, 91);
|
||||
int screenX = 0;
|
||||
int screenY = _random.Next(2) == 0 ? 0 : 30;
|
||||
int sizeWidth = _random.Next(1024, 1921);
|
||||
int sizeHeight = _random.Next(768, 1081);
|
||||
int availWidth = _random.Next(1280, 1921);
|
||||
int availHeight = _random.Next(800, 1081);
|
||||
|
||||
return $"{innerWidth}|{innerHeight}|{outerWidth}|{outerHeight}|" +
|
||||
$"{screenX}|{screenY}|0|0|{sizeWidth}|{sizeHeight}|" +
|
||||
$"{availWidth}|{availHeight}|{innerWidth}|{innerHeight}|24|24|{platform}";
|
||||
}
|
||||
}
|
||||
|
||||
public class ABogus2
|
||||
{
|
||||
private int aid = 6383;
|
||||
private int pageId = 0;
|
||||
private string salt = "cus";
|
||||
private bool boe = false;
|
||||
private double ddrt = 8.5;
|
||||
private double ic = 8.5;
|
||||
private List<string> paths = new() {
|
||||
"^/webcast/", "^/aweme/v1/", "^/aweme/v2/", "/v1/message/send", "^/live/", "^/captcha/", "^/ecom/"
|
||||
};
|
||||
|
||||
private byte[] uaKey = { 0x00, 0x01, 0x0E };
|
||||
|
||||
private string character = "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe";
|
||||
private string character2 = "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe";
|
||||
|
||||
private List<string> characterList;
|
||||
private CryptoUtility cryptoUtility;
|
||||
|
||||
private string userAgent;
|
||||
private string browserFp;
|
||||
|
||||
private int[] sortIndex = {
|
||||
18, 20, 52, 26, 30, 34, 58, 38, 40, 53, 42, 21, 27, 54, 55, 31, 35, 57, 39, 41, 43, 22, 28,
|
||||
32, 60, 36, 23, 29, 33, 37, 44, 45, 59, 46, 47, 48, 49, 50, 24, 25, 65, 66, 70, 71
|
||||
};
|
||||
|
||||
private int[] sortIndex2 = {
|
||||
18, 20, 26, 30, 34, 38, 40, 42, 21, 27, 31, 35, 39, 41, 43, 22, 28, 32, 36, 23, 29, 33, 37,
|
||||
44, 45, 46, 47, 48, 49, 50, 24, 25, 52, 53, 54, 55, 57, 58, 59, 60, 65, 66, 70, 71
|
||||
};
|
||||
|
||||
public List<int> Options { get; set; } = new() { 0, 1, 14 }; // POST 默认
|
||||
|
||||
public ABogus2(string fp = "", string userAgent = "", List<int> options = null)
|
||||
{
|
||||
if (options != null) Options = options;
|
||||
|
||||
this.userAgent = !string.IsNullOrEmpty(userAgent)
|
||||
? userAgent
|
||||
: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0";
|
||||
|
||||
this.browserFp = !string.IsNullOrEmpty(fp)
|
||||
? fp
|
||||
: BrowserFingerprintGenerator.GenerateFingerprint("Edge");
|
||||
|
||||
characterList = new List<string> { character, character2 };
|
||||
cryptoUtility = new CryptoUtility(salt, characterList);
|
||||
}
|
||||
|
||||
public string EncodeData(string data, int alphabetIndex = 0)
|
||||
{
|
||||
return cryptoUtility.AbogusEncode(data, alphabetIndex);
|
||||
}
|
||||
|
||||
public (string paramsWithAbogus, string abogus, string userAgent, string body) GenerateAbogus(string paramsStr, string body = "")
|
||||
{
|
||||
var abDir = new Dictionary<int, object>
|
||||
{
|
||||
{ 8, 3 },
|
||||
{ 15, new {
|
||||
aid = this.aid,
|
||||
pageId = this.pageId,
|
||||
boe = this.boe,
|
||||
ddrt = this.ddrt,
|
||||
paths = this.paths,
|
||||
track = new { mode = 0, delay = 300, paths = new List<object>() },
|
||||
dump = true,
|
||||
rpU = ""
|
||||
}},
|
||||
{ 18, 44 },
|
||||
{ 19, new[] { 1, 0, 1, 0, 1 } },
|
||||
{ 66, 0 },
|
||||
{ 69, 0 },
|
||||
{ 70, 0 },
|
||||
{ 71, 0 }
|
||||
};
|
||||
|
||||
long startEncryption = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
int[] array1 = cryptoUtility.ParamsToArray(paramsStr); // 双重哈希
|
||||
int[] array2 = body != "" ? cryptoUtility.ParamsToArray(body) : new int[0];
|
||||
string encodedUa = cryptoUtility.Base64Encode(
|
||||
StringProcessor.ToCharStr(
|
||||
Array.ConvertAll(CryptoUtility.Rc4Encrypt(uaKey, userAgent), b => (int)b)
|
||||
),
|
||||
1
|
||||
);
|
||||
|
||||
int[] array3 = cryptoUtility.Sm3ToArray(encodedUa); // 不加盐
|
||||
|
||||
long endEncryption = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
|
||||
// 插入时间戳高位
|
||||
abDir[20] = (byte)((startEncryption >> 24) & 0xFF);
|
||||
abDir[21] = (byte)((startEncryption >> 16) & 0xFF);
|
||||
abDir[22] = (byte)((startEncryption >> 8) & 0xFF);
|
||||
abDir[23] = (byte)(startEncryption & 0xFF);
|
||||
abDir[24] = (int)((startEncryption >> 32) & 0xFF);
|
||||
abDir[25] = (int)((startEncryption >> 40) & 0xFF);
|
||||
|
||||
// 请求选项
|
||||
abDir[26] = (byte)((Options[0] >> 24) & 0xFF);
|
||||
abDir[27] = (byte)((Options[0] >> 16) & 0xFF);
|
||||
abDir[28] = (byte)((Options[0] >> 8) & 0xFF);
|
||||
abDir[29] = (byte)(Options[0] & 0xFF);
|
||||
|
||||
abDir[30] = (byte)((Options[1] >> 8) & 0xFF);
|
||||
abDir[31] = (byte)(Options[1] & 0xFF);
|
||||
abDir[32] = (byte)((Options[1] >> 24) & 0xFF);
|
||||
abDir[33] = (byte)((Options[1] >> 16) & 0xFF);
|
||||
|
||||
abDir[34] = (byte)((Options[2] >> 24) & 0xFF);
|
||||
abDir[35] = (byte)((Options[2] >> 16) & 0xFF);
|
||||
abDir[36] = (byte)((Options[2] >> 8) & 0xFF);
|
||||
abDir[37] = (byte)(Options[2] & 0xFF);
|
||||
|
||||
abDir[38] = array1[21];
|
||||
abDir[39] = array1[22];
|
||||
abDir[40] = array2.Length > 21 ? array2[21] : 0;
|
||||
abDir[41] = array2.Length > 22 ? array2[22] : 0;
|
||||
abDir[42] = array3.Length > 23 ? array3[23] : 0;
|
||||
abDir[43] = array3.Length > 24 ? array3[24] : 0;
|
||||
|
||||
abDir[44] = (byte)((endEncryption >> 24) & 0xFF);
|
||||
abDir[45] = (byte)((endEncryption >> 16) & 0xFF);
|
||||
abDir[46] = (byte)((endEncryption >> 8) & 0xFF);
|
||||
abDir[47] = (byte)(endEncryption & 0xFF);
|
||||
abDir[48] = abDir[8];
|
||||
abDir[49] = (int)((endEncryption >> 32) & 0xFF);
|
||||
abDir[50] = (int)((endEncryption >> 40) & 0xFF);
|
||||
|
||||
abDir[51] = (byte)((pageId >> 24) & 0xFF);
|
||||
abDir[52] = (byte)((pageId >> 16) & 0xFF);
|
||||
abDir[53] = (byte)((pageId >> 8) & 0xFF);
|
||||
abDir[54] = (byte)(pageId & 0xFF);
|
||||
abDir[55] = pageId;
|
||||
abDir[56] = aid;
|
||||
abDir[57] = (byte)(aid & 0xFF);
|
||||
abDir[58] = (byte)((aid >> 8) & 0xFF);
|
||||
abDir[59] = (byte)((aid >> 16) & 0xFF);
|
||||
abDir[60] = (byte)((aid >> 24) & 0xFF);
|
||||
|
||||
abDir[64] = browserFp.Length;
|
||||
abDir[65] = browserFp.Length;
|
||||
|
||||
// 排序取值
|
||||
var sortedValues = sortIndex
|
||||
.Select(k => Convert.ToInt32(abDir.GetValueOrDefault(k, 0)))
|
||||
.ToList();
|
||||
|
||||
var fpArray = StringProcessor.ToOrdArray(browserFp).ToList();
|
||||
|
||||
int abXor = 0;
|
||||
abXor = sortIndex2
|
||||
.Select(k => Convert.ToInt32(abDir.GetValueOrDefault(k, 0)))
|
||||
.Aggregate(0, (x, y) => x ^ y);
|
||||
|
||||
|
||||
sortedValues.AddRange(fpArray);
|
||||
sortedValues.Add(abXor);
|
||||
|
||||
string randomBytes = StringProcessor.GenerateRandomBytes();
|
||||
string transformed = cryptoUtility.TransformBytes(sortedValues.ToArray());
|
||||
|
||||
string abogusBytesStr = randomBytes + transformed;
|
||||
string abogus = cryptoUtility.AbogusEncode(abogusBytesStr, 0);
|
||||
|
||||
string finalParams = $"{paramsStr}&a_bogus={abogus}";
|
||||
|
||||
return (finalParams, abogus, userAgent, body);
|
||||
}
|
||||
}
|
||||
|
||||
// 测试代码
|
||||
//public class ABogusTest
|
||||
//{
|
||||
// //public static void Main()
|
||||
// //{
|
||||
// // string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0";
|
||||
// // string edgeFp = BrowserFingerprintGenerator.GenerateFingerprint("Edge");
|
||||
// // var abogus = new ABogus2(fp: edgeFp, userAgent: userAgent);
|
||||
|
||||
// // // GET请求测试
|
||||
// // string getParams = "device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=MS4wLjABAAAArDVBosPJF3eIWVEFp0szuJ-e1V_-rK0ieJeWwpE77E8&max_cursor=0&locate_query=false&show_live_replay_strategy=1&need_time_list=1&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&from_user_page=1&update_version_code=170400&pc_client_type=1&pc_libra_divert=Windows&support_h265=1&support_dash=0&version_code=290100&version_name=29.1.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=131.0.0.0&browser_online=true&engine_name=Blink&engine_version=131.0.0.0&os_name=Windows&os_version=10&cpu_core_num=12&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50";
|
||||
// // var getResult = abogus.GenerateABogus(getParams);
|
||||
// // Console.WriteLine($"GET 完整URL: https://www.douyin.com/aweme/v1/web/aweme/detail/?{getResult.Params}");
|
||||
// // Console.WriteLine($"GET ABogus: {getResult.ABogus}");
|
||||
|
||||
// // // POST请求测试
|
||||
// // string postParams = "device_platform=webapp&aid=6383&channel=channel_pc_web&pc_client_type=1&pc_libra_divert=Windows&update_version_code=170400&support_h265=1&support_dash=0&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=131.0.0.0&browser_online=true&engine_name=Blink&engine_version=131.0.0.0&os_name=Windows&os_version=10&cpu_core_num=12&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50";
|
||||
// // string postBody = "aweme_type=0&item_id=7467485482314763572&play_delta=1&source=0";
|
||||
// // var postResult = abogus.GenerateABogus(postParams, postBody);
|
||||
// // Console.WriteLine($"POST 完整URL: https://www.douyin.com/aweme/v2/web/aweme/stats/?{postResult.Params}");
|
||||
// // Console.WriteLine($"POST ABogus: {postResult.ABogus}");
|
||||
// // Console.WriteLine($"POST Body: {postResult.Body}");
|
||||
// //}
|
||||
//}
|
||||
}
|
||||
|
||||
+268
-154
@@ -1,18 +1,95 @@
|
||||
using System.Threading.Channels;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace dy.net.utils
|
||||
{
|
||||
public class DouyinBaseParamDics
|
||||
/// <summary>
|
||||
/// 抖音网页端请求参数字典管理类
|
||||
/// 提供各类接口的标准化参数字典,对外暴露的属性名称和返回类型保持不变
|
||||
/// </summary>
|
||||
public static class DouyinBaseParamDics
|
||||
{
|
||||
public static Dictionary<string, string> CollectParams { get; } = InitializeUserCollecParams();
|
||||
/// <summary>
|
||||
/// 收藏列表参数(用户收藏的内容)
|
||||
/// </summary>
|
||||
public static Dictionary<string, string> CollectParams { get; } = InitializeUserCollectParams();
|
||||
|
||||
/// <summary>
|
||||
/// 用户收藏参数(与 CollectParams 功能区分,保留原始定义)
|
||||
/// </summary>
|
||||
public static Dictionary<string, string> FavoriteParams { get; } = InitializeUserFavoriteParams();
|
||||
|
||||
/// <summary>
|
||||
/// 博主发布作品参数
|
||||
/// </summary>
|
||||
public static Dictionary<string, string> UpderPostParams { get; } = InitializeUpderPostParams();
|
||||
|
||||
private static Dictionary<string, string> InitializeUserCollecParams()
|
||||
{
|
||||
var parameters = GetDyBaseParameters();
|
||||
/// <summary>
|
||||
/// 我关注的博主列表参数
|
||||
/// </summary>
|
||||
public static Dictionary<string, string> MyFollowParams { get; } = InitializeMyFollowParams();
|
||||
|
||||
// 覆盖与基础字段不同的值
|
||||
/// <summary>
|
||||
/// 视频详情
|
||||
/// </summary>
|
||||
public static Dictionary<string, string> ViedoDetailParam { get; } = InitializeViedoDetailParam();
|
||||
|
||||
/// <summary>
|
||||
/// 抖音作品列表请求参数(适配接口:https://www.douyin.com/aweme/v1/web/aweme/post/)
|
||||
/// </summary>
|
||||
public static Dictionary<string, string> InitializeDouyinPostParams()
|
||||
{
|
||||
// 基于基础参数扩展,减少冗余
|
||||
var parameters = GetBaseParameters();
|
||||
|
||||
// 覆盖基础参数中不同的值
|
||||
parameters["version_code"] = "290100";
|
||||
parameters["version_name"] = "29.1.0";
|
||||
parameters["screen_width"] = "1707";
|
||||
parameters["screen_height"] = "1067";
|
||||
parameters["browser_name"] = "Chrome";
|
||||
parameters["browser_version"] = "142.0.0.0";
|
||||
parameters["engine_version"] = "142.0.0.0";
|
||||
parameters["cpu_core_num"] = "32";
|
||||
parameters["support_h265"] = "1";
|
||||
parameters["support_dash"] = "1";
|
||||
|
||||
// 添加特有参数
|
||||
parameters.AddRange(new Dictionary<string, string>
|
||||
{
|
||||
{"sec_user_id", ""},
|
||||
{"max_cursor", "0"},
|
||||
{"locate_item_id", "7576282367263807451"},
|
||||
{"locate_query", "false"},
|
||||
{"count", "18"},
|
||||
{"show_live_replay_strategy", "1"},
|
||||
{"need_time_list", "1"},
|
||||
{"time_list_query", "0"},
|
||||
{"publish_video_strategy_type", "2"},
|
||||
{"from_user_page", "1"},
|
||||
{"webid", "7574080345697584675"},
|
||||
{"uifid", ""},
|
||||
{"msToken", ""},
|
||||
{"a_bogus", ""},
|
||||
{"verifyFp", ""},
|
||||
{"fp", ""},
|
||||
{"x-secsdk-web-expire", ""},
|
||||
{"x-secsdk-web-signature", ""},
|
||||
{"cut_version", "1"}
|
||||
});
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
#region 私有初始化方法
|
||||
|
||||
/// <summary>
|
||||
/// 初始化收藏列表参数
|
||||
/// </summary>
|
||||
private static Dictionary<string, string> InitializeUserCollectParams()
|
||||
{
|
||||
var parameters = GetBaseParameters();
|
||||
|
||||
// 覆盖基础参数
|
||||
parameters["version_code"] = "290100";
|
||||
parameters["version_name"] = "29.1.0";
|
||||
parameters["screen_width"] = "1920";
|
||||
@@ -21,22 +98,27 @@ namespace dy.net.utils
|
||||
parameters["engine_version"] = "130.0.0.0";
|
||||
parameters["cpu_core_num"] = "12";
|
||||
|
||||
// 添加当前字段特有的键值对
|
||||
parameters["from_user_page"] = "1";
|
||||
parameters["locate_query"] = "false";
|
||||
parameters["need_time_list"] = "1";
|
||||
parameters["show_live_replay_strategy"] = "1";
|
||||
parameters["time_list_query"] = "0";
|
||||
// 添加特有参数
|
||||
parameters.AddRange(new Dictionary<string, string>
|
||||
{
|
||||
{"from_user_page", "1"},
|
||||
{"locate_query", "false"},
|
||||
{"need_time_list", "1"},
|
||||
{"show_live_replay_strategy", "1"},
|
||||
{"time_list_query", "0"}
|
||||
});
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
// 初始化用户收藏参数
|
||||
/// <summary>
|
||||
/// 初始化用户收藏参数
|
||||
/// </summary>
|
||||
private static Dictionary<string, string> InitializeUserFavoriteParams()
|
||||
{
|
||||
var parameters = GetDyBaseParameters();
|
||||
var parameters = GetBaseParameters();
|
||||
|
||||
// 覆盖与基础字段不同的值
|
||||
// 覆盖基础参数
|
||||
parameters["version_code"] = "170400";
|
||||
parameters["version_name"] = "17.4.0";
|
||||
parameters["screen_width"] = "1536";
|
||||
@@ -44,154 +126,121 @@ namespace dy.net.utils
|
||||
parameters["browser_version"] = "140.0.0.0";
|
||||
parameters["engine_version"] = "140.0.0.0";
|
||||
parameters["cpu_core_num"] = "20";
|
||||
|
||||
// 添加当前字段特有的键值对
|
||||
parameters["min_cursor"] = "0";
|
||||
parameters["cut_version"] = "1";
|
||||
parameters["count"] = "18";
|
||||
parameters["support_h265"] = "1";
|
||||
parameters["support_dash"] = "1";
|
||||
|
||||
// 添加特有参数
|
||||
parameters.AddRange(new Dictionary<string, string>
|
||||
{
|
||||
{"min_cursor", "0"},
|
||||
{"cut_version", "1"},
|
||||
{"count", "18"}
|
||||
});
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
// 初始化抖音博主发布作品参数
|
||||
private static Dictionary<string,string> InitializeUpderPostParams()
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "WebIdLastTime", "1714385892" },
|
||||
{ "aid", "1988" },
|
||||
{ "app_language", "zh-Hans" },
|
||||
{ "app_name", "tiktok_web" },
|
||||
{ "browser_language", "zh-CN" },
|
||||
{ "browser_name", "Mozilla" },
|
||||
{ "browser_online", "true" },
|
||||
{ "browser_platform", "Win32" },
|
||||
{ "browser_version", "5.0%20%28Windows%29" },
|
||||
{ "cookie_enabled", "true" },
|
||||
{ "count", "18" },
|
||||
{ "coverFormat", "2" },
|
||||
{ "cursor", "0" },
|
||||
{ "data_collection_enabled", "true" },
|
||||
{ "device_id", "7380187414842836523" },
|
||||
{ "device_platform", "webapp" },
|
||||
{ "channel", "channel_pc_web" },
|
||||
{ "focus_state", "true" },
|
||||
{ "from_page", "user" },
|
||||
{ "history_len", "3" },
|
||||
{ "is_fullscreen", "false" },
|
||||
{ "is_page_visible", "true" },
|
||||
{ "language", "zh-Hans" },
|
||||
{ "locate_item_id", "" },
|
||||
{ "needPinnedItemIds", "true" },
|
||||
{ "odinId", "7404669909585003563" },
|
||||
{ "os", "windows" },
|
||||
{ "post_item_list_request_type", "0" },
|
||||
{ "priority_region", "US" },
|
||||
{ "referer", "" },
|
||||
{ "region", "US" },
|
||||
{ "screen_height", "827" },
|
||||
{ "screen_width", "1323" },
|
||||
{ "secUid", "" },
|
||||
{ "tz_name", "America%2FLos_Angeles" },
|
||||
{ "user_is_login", "true" },
|
||||
{ "webcast_language", "zh-Hans" },
|
||||
{ "msToken", "" },
|
||||
{ "_signature", "_02B4Z6wo000017oyWOQAAIDD9xNhTSnfaDu6MFxAAIlj23" },
|
||||
{"sec_user_id",""}
|
||||
};
|
||||
return parameters;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 初始化抖音网页端用户作品列表请求参数(参数来源于目标URL)
|
||||
/// 适配接口:https://www.douyin.com/aweme/v1/web/aweme/post/
|
||||
/// 初始化博主发布作品参数
|
||||
/// </summary>
|
||||
/// <returns>抖音作品列表请求参数字典</returns>
|
||||
public static Dictionary<string, string> InitializeDouyinPostParams()
|
||||
private static Dictionary<string, string> InitializeUpderPostParams()
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
// 该接口参数差异较大,单独初始化(保留原始参数完整)
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{"WebIdLastTime", "1714385892"},
|
||||
{"aid", "1988"},
|
||||
{"app_language", "zh-Hans"},
|
||||
{"app_name", "tiktok_web"},
|
||||
{"browser_language", "zh-CN"},
|
||||
{"browser_name", "Mozilla"},
|
||||
{"browser_online", "true"},
|
||||
{"browser_platform", "Win32"},
|
||||
{"browser_version", "5.0%20%28Windows%29"},
|
||||
{"cookie_enabled", "true"},
|
||||
{"count", "18"},
|
||||
{"coverFormat", "2"},
|
||||
{"cursor", "0"},
|
||||
{"data_collection_enabled", "true"},
|
||||
{"device_id", "7380187414842836523"},
|
||||
{"device_platform", "webapp"},
|
||||
{"channel", "channel_pc_web"},
|
||||
{"focus_state", "true"},
|
||||
{"from_page", "user"},
|
||||
{"history_len", "3"},
|
||||
{"is_fullscreen", "false"},
|
||||
{"is_page_visible", "true"},
|
||||
{"language", "zh-Hans"},
|
||||
{"locate_item_id", ""},
|
||||
{"needPinnedItemIds", "true"},
|
||||
{"odinId", "7404669909585003563"},
|
||||
{"os", "windows"},
|
||||
{"post_item_list_request_type", "0"},
|
||||
{"priority_region", "US"},
|
||||
{"referer", ""},
|
||||
{"region", "US"},
|
||||
{"screen_height", "827"},
|
||||
{"screen_width", "1323"},
|
||||
{"secUid", ""},
|
||||
{"sec_user_id", ""},
|
||||
{"tz_name", "America%2FLos_Angeles"},
|
||||
{"user_is_login", "true"},
|
||||
{"webcast_language", "zh-Hans"},
|
||||
{"msToken", ""}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化我关注的博主列表参数
|
||||
/// </summary>
|
||||
private static Dictionary<string, string> InitializeMyFollowParams()
|
||||
{
|
||||
// 基础设备与渠道参数
|
||||
{ "device_platform", "webapp" },
|
||||
{ "aid", "6383" },
|
||||
{ "channel", "channel_pc_web" },
|
||||
|
||||
// 用户标识参数
|
||||
{ "sec_user_id", "" },
|
||||
|
||||
// 分页与内容定位参数
|
||||
{ "max_cursor", "0" },
|
||||
{ "locate_item_id", "7576282367263807451" },
|
||||
{ "locate_query", "false" },
|
||||
{ "count", "18" },
|
||||
|
||||
// 视频相关配置参数
|
||||
{ "show_live_replay_strategy", "1" },
|
||||
{ "need_time_list", "1" },
|
||||
{ "time_list_query", "0" },
|
||||
{ "publish_video_strategy_type", "2" },
|
||||
{ "support_h265", "1" },
|
||||
{ "support_dash", "1" },
|
||||
|
||||
// 版本与更新参数
|
||||
{ "from_user_page", "1" },
|
||||
{ "update_version_code", "170400" },
|
||||
{ "version_code", "290100" },
|
||||
{ "version_name", "29.1.0" },
|
||||
|
||||
// PC端特有参数
|
||||
{ "pc_client_type", "1" },
|
||||
{ "pc_libra_divert", "Windows" },
|
||||
{ "cpu_core_num", "32" },
|
||||
|
||||
// 浏览器环境参数
|
||||
{ "browser_language", "zh-CN" },
|
||||
{ "browser_platform", "Win32" },
|
||||
{ "browser_name", "Chrome" },
|
||||
{ "browser_version", "142.0.0.0" },
|
||||
{ "browser_online", "true" },
|
||||
{ "engine_name", "Blink" },
|
||||
{ "engine_version", "142.0.0.0" },
|
||||
|
||||
// 系统环境参数
|
||||
{ "os_name", "Windows" },
|
||||
{ "os_version", "10" },
|
||||
{ "device_memory", "8" },
|
||||
{ "platform", "PC" },
|
||||
{ "cookie_enabled", "true" },
|
||||
|
||||
// 屏幕与网络参数
|
||||
{ "screen_width", "1707" },
|
||||
{ "screen_height", "1067" },
|
||||
{ "downlink", "10" },
|
||||
{ "effective_type", "4g" },
|
||||
{ "round_trip_time", "0" },
|
||||
|
||||
// 用户唯一标识参数
|
||||
{ "webid", "7574080345697584675" },
|
||||
{ "uifid", "" },
|
||||
|
||||
// 安全验证与签名参数
|
||||
{ "msToken", "" },
|
||||
{ "a_bogus", "" },
|
||||
{ "verifyFp", "" },
|
||||
{ "fp", "" },
|
||||
{ "x-secsdk-web-expire", "" },
|
||||
{ "x-secsdk-web-signature", "" },
|
||||
|
||||
// 其他辅助参数
|
||||
{ "whale_cut_token", "" },
|
||||
{ "cut_version", "1" }
|
||||
};
|
||||
var parameters = GetBaseParameters();
|
||||
|
||||
// 覆盖基础参数
|
||||
parameters["version_code"] = "170400";
|
||||
parameters["version_name"] = "17.4.0";
|
||||
parameters["screen_width"] = "1707";
|
||||
parameters["screen_height"] = "1067";
|
||||
parameters["browser_name"] = "Edge";
|
||||
parameters["browser_version"] = "141.0.0.0";
|
||||
parameters["engine_version"] = "141.0.0.0";
|
||||
parameters["cpu_core_num"] = "32";
|
||||
parameters["support_h265"] = "1";
|
||||
parameters["support_dash"] = "1";
|
||||
|
||||
// 添加特有参数
|
||||
parameters.AddRange(new Dictionary<string, string>
|
||||
{
|
||||
{"user_id", "1218695735550247"},
|
||||
{"sec_user_id", ""},
|
||||
{"offset", "40"},
|
||||
{"min_time", "0"},
|
||||
{"max_time", "0"},
|
||||
{"count", "20"},
|
||||
{"source_type", "4"},
|
||||
{"gps_access", "0"},
|
||||
{"address_book_access", "0"},
|
||||
{"is_top", "1"},
|
||||
{"webid", "7577203855940994560"},
|
||||
{"uifid", ""},
|
||||
{"msToken", ""},
|
||||
{"a_bogus", ""},
|
||||
{"verifyFp", ""},
|
||||
{"fp", ""}
|
||||
});
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
// 静态基础参数(供内部初始化使用)
|
||||
private static Dictionary<string, string> GetDyBaseParameters()
|
||||
#endregion
|
||||
|
||||
#region 基础参数与扩展方法
|
||||
|
||||
/// <summary>
|
||||
/// 获取抖音网页端基础参数字典(所有接口的公共参数)
|
||||
/// </summary>
|
||||
private static Dictionary<string, string> GetBaseParameters()
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
@@ -199,6 +248,7 @@ namespace dy.net.utils
|
||||
{"aid", "6383"},
|
||||
{"channel", "channel_pc_web"},
|
||||
{"pc_client_type", "1"},
|
||||
{"pc_libra_divert", "Windows"},
|
||||
{"cookie_enabled", "true"},
|
||||
{"browser_language", "zh-CN"},
|
||||
{"browser_platform", "Win32"},
|
||||
@@ -211,13 +261,77 @@ namespace dy.net.utils
|
||||
{"platform", "PC"},
|
||||
{"downlink", "10"},
|
||||
{"effective_type", "4g"},
|
||||
{"pc_libra_divert", "Windows"},
|
||||
{"publish_video_strategy_type", "2"},
|
||||
{"round_trip_time", "0"},
|
||||
{"whale_cut_token", ""},
|
||||
{"update_version_code", "170400"}
|
||||
{"update_version_code", "170400"},
|
||||
{"whale_cut_token", ""}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static Dictionary<string, string> InitializeViedoDetailParam()
|
||||
{
|
||||
|
||||
Dictionary<string, string> requestParams = new Dictionary<string, string>
|
||||
{
|
||||
{ "device_platform", "webapp" },
|
||||
{ "aid", "6383" },
|
||||
{ "channel", "channel_pc_web" },
|
||||
{ "aweme_id", "" },
|
||||
{ "request_source", "600" },
|
||||
{ "origin_type", "video_page" },
|
||||
{ "update_version_code", "170400" },
|
||||
{ "pc_client_type", "1" },
|
||||
{ "pc_libra_divert", "Windows" },
|
||||
{ "support_h265", "1" },
|
||||
{ "support_dash", "1" },
|
||||
{ "cpu_core_num", "32" },
|
||||
{ "version_code", "190500" },
|
||||
{ "version_name", "19.5.0" },
|
||||
{ "cookie_enabled", "true" },
|
||||
{ "screen_width", "1707" },
|
||||
{ "screen_height", "1067" },
|
||||
{ "browser_language", "zh-CN" },
|
||||
{ "browser_platform", "Win32" },
|
||||
{ "browser_name", "Edge" },
|
||||
{ "browser_version", "141.0.0.0" },
|
||||
{ "browser_online", "true" },
|
||||
{ "engine_name", "Blink" },
|
||||
{ "engine_version", "141.0.0.0" },
|
||||
{ "os_name", "Windows" },
|
||||
{ "os_version", "10" },
|
||||
{ "device_memory", "8" },
|
||||
{ "platform", "PC" },
|
||||
{ "downlink", "10" },
|
||||
{ "effective_type", "4g" },
|
||||
{ "round_trip_time", "0" },
|
||||
{ "webid", "7577203855940994560" },
|
||||
{ "uifid", "0e81ba593d64ebaca259bdbe302de8d7e55ac2e982f7412f10fbc5c77c64bb8b8830ffed112ea8b3bc54f3b6e2af3856daafa4efaafef18953612820da493aeaebdeac0537b99b8e68f343c2476cf6347f7751a87d92952128116470f147215cf46b949f43f31aedcb660fd3c5c7eb2145183cc93d1b4202205b4af7d7c69ab55e860f96e315899a2ee74a262273694ec973ba682bb6fdc351e0e66250b21cf9aef3ccaa3e0c28b085fd095e947d92a5336b9e65706d649a7b79541feab3487f" },
|
||||
{ "verifyFp", "verify_migsr5je_BJ1YiVbY_uR2U_4VXu_8Uje_GgwhVf7Y6hb8" },
|
||||
{ "fp", "verify_migsr5je_BJ1YiVbY_uR2U_4VXu_8Uje_GgwhVf7Y6hb8" },
|
||||
//{ "msToken", "zVKqVOb-uBF-Opse1B5y9u0hw7SlRa49w8HB56cVSY0Uymjqs17JJ9qOUXv7IK0j3de1ErBjSVAm0JSd5SH1sYg7jeriVvGNJoDUM3dKwVCrqlapZgU9g2aYAeHs5oHddTGtZ3Pri9MIRltXVRdirZWMewfH8MlqmIVaCiTuGzRcZ6va9aWe0CyR" },
|
||||
//{ "a_bogus", "dy45kFWjOxRfOdFtmOnc9WxlY8L%2FNTuy6Pi2SYAP9PKGcwFcaWNpBNCfrxLuRUd%2FzuBzhe3HqdlMYDnc0zX0ZenkKmkkupv6Bt%2FC9L0LZZHvbBJZ7rgiemSxzk4O8KsOmAIbiM75AsBEIxo5VrCwAdlCu%2F-xBbmD%2Fp3vVATCE2ysUAujwn%2FVa-JDNw7qaf%3D%3D" },
|
||||
//{ "x-secsdk-web-expire", "1764378577130" },
|
||||
//{ "x-secsdk-web-signature", "3b8aa32e9457ebe3b65afdae11e2fe86" }
|
||||
};
|
||||
return requestParams;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 字典扩展方法:批量添加键值对(避免重复 Add 调用)
|
||||
/// </summary>
|
||||
private static void AddRange(this Dictionary<string, string> target, Dictionary<string, string> source)
|
||||
{
|
||||
foreach (var item in source)
|
||||
{
|
||||
// 避免键重复(如果有重复以源字典为准)
|
||||
if (target.ContainsKey(item.Key))
|
||||
target[item.Key] = item.Value;
|
||||
else
|
||||
target.Add(item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace dy.net.utils
|
||||
/// <summary>
|
||||
/// 抖音标题转文件名工具类(兼容Windows/macOS/Linux)
|
||||
/// </summary>
|
||||
public static class TikTokFileNameHelper
|
||||
public static class DouyinFileNameHelper
|
||||
{
|
||||
#region 配置参数
|
||||
/// <summary>
|
||||
@@ -153,9 +153,9 @@ namespace dy.net.utils
|
||||
{
|
||||
path = path.Replace(c, '_');
|
||||
}
|
||||
if (path.Length > 50)
|
||||
if (path.Length > 100)
|
||||
{
|
||||
path = path.Substring(0, 50);
|
||||
path = path.Substring(0, 100);
|
||||
}
|
||||
return path.Trim().Replace(" ", "");
|
||||
}
|
||||
@@ -164,5 +164,31 @@ namespace dy.net.utils
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 检查字符串是否仅包含字母、数字、简体中文(无特殊字符)
|
||||
/// </summary>
|
||||
/// <param name="input">待检查的字符串</param>
|
||||
/// <returns>true:无特殊字符(仅允许字符);false:含有特殊字符</returns>
|
||||
public static bool IsValidWithoutSpecialChars(string input)
|
||||
{
|
||||
// 空字符串默认返回 true(若需禁止空字符串,可先判断 string.IsNullOrWhiteSpace(input) 并返回 false)
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return true;
|
||||
|
||||
// 正则表达式说明:
|
||||
// ^ :匹配字符串开头
|
||||
// $ :匹配字符串结尾
|
||||
// [a-zA-Z0-9\u4E00-\u9FA5] :允许的字符范围
|
||||
// a-zA-Z:大小写字母
|
||||
// 0-9:数字
|
||||
// \u4E00-\u9FA5:简体中文 Unicode 核心范围(覆盖99%+简体中文常用字)
|
||||
// * :匹配 0 个或多个允许的字符(若需至少1个字符,可改为 +)
|
||||
const string pattern = @"^[a-zA-Z0-9\u4E00-\u9FA5]*$";
|
||||
|
||||
// 忽略文化差异,仅按字符编码匹配
|
||||
return Regex.IsMatch(input, pattern, RegexOptions.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace dy.net.utils
|
||||
{
|
||||
public class FFmpegHelper : IDisposable
|
||||
{
|
||||
#if DEBUG
|
||||
// Debug 环境,通常是 Windows
|
||||
private readonly string _ffmpegExecutablePath = "E:\\down\\ffmpeg\\bin\\ffmpeg.exe";
|
||||
private readonly string _ffprobeExecutablePath = "E:\\down\\ffmpeg\\bin\\ffprobe.exe";
|
||||
#else
|
||||
// Release 环境,通常是 Docker Linux
|
||||
private readonly string _ffmpegExecutablePath = "ffmpeg";
|
||||
private readonly string _ffprobeExecutablePath = "ffprobe";
|
||||
#endif
|
||||
|
||||
|
||||
private Process _ffmpegProcess;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
// 视频参数
|
||||
public int VideoWidth { get; set; } = 1080;
|
||||
public int VideoHeight { get; set; } = 1920;
|
||||
public int OutputFrameRate { get; set; } = 30;
|
||||
public int ImageDisplayDurationSeconds { get; set; } = 2;
|
||||
|
||||
// 编码参数
|
||||
public string VideoCodec { get; set; } = "libx264";
|
||||
public string VideoPreset { get; set; } = "medium";
|
||||
public int VideoCrf { get; set; } = 23;
|
||||
public string AudioCodec { get; set; } = "aac";
|
||||
public string AudioBitrate { get; set; } = "192k";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 将多张图片和一个音频文件合成为视频(最终终极版)。
|
||||
/// </summary>
|
||||
public async Task<string> CreateVideoFromImagesAndAudioAsync(
|
||||
IEnumerable<string> imageFilePaths,
|
||||
string audioFilePath,
|
||||
string outputVideoPath,
|
||||
int VideoWidth = 1080,
|
||||
int Height = 1920,
|
||||
IProgress<double> progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 输入验证
|
||||
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));
|
||||
|
||||
foreach (var imagePath in imageFilePaths)
|
||||
{
|
||||
if (!File.Exists(imagePath))
|
||||
throw new FileNotFoundException("图片文件未找到。", imagePath);
|
||||
}
|
||||
|
||||
var outputDirectory = Path.GetDirectoryName(outputVideoPath);
|
||||
if (!string.IsNullOrEmpty(outputDirectory) && !Directory.Exists(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
|
||||
// 关键步骤 1: 创建临时目录并生成有序图片序列
|
||||
string tempImageDir = Path.Combine(AppContext.BaseDirectory, "temp", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempImageDir);
|
||||
|
||||
var imageList = imageFilePaths.ToList();
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < imageList.Count; i++)
|
||||
{
|
||||
string sourcePath = imageList[i];
|
||||
string extension = Path.GetExtension(sourcePath);
|
||||
string destFileName = $"temp_{i + 1:D3}{extension}";
|
||||
string destPath = Path.Combine(tempImageDir, destFileName);
|
||||
File.Copy(sourcePath, destPath);
|
||||
}
|
||||
|
||||
string imageSequencePattern = Path.Combine(tempImageDir, "temp_%03d" + Path.GetExtension(imageList[0]));
|
||||
double imageFps = Math.Round(1.0 / ImageDisplayDurationSeconds, 2);
|
||||
|
||||
// --- 修正点 1: 整合音频时长获取逻辑 ---
|
||||
// 我们不再需要 GetAudioFilterAsync,而是直接获取时长用于计算视频循环
|
||||
double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds; // 默认值为图片总时长
|
||||
try
|
||||
{
|
||||
// 使用 ffprobe 获取音频时长
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = _ffprobeExecutablePath,
|
||||
Arguments = $"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"{audioFilePath}\"",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using (var process = new Process { StartInfo = startInfo })
|
||||
{
|
||||
process.Start();
|
||||
string output = await process.StandardOutput.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (double.TryParse(output, out double duration))
|
||||
{
|
||||
audioDurationSeconds = duration;
|
||||
//Console.WriteLine($"成功获取音频时长: {audioDurationSeconds:F2}s");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}");
|
||||
}
|
||||
|
||||
// 计算图片序列需要循环的次数
|
||||
int loopCount = 1;
|
||||
double singleLoopDuration = imageList.Count * ImageDisplayDurationSeconds;
|
||||
if (audioDurationSeconds > singleLoopDuration)
|
||||
{
|
||||
loopCount = (int)Math.Ceiling(audioDurationSeconds / singleLoopDuration);
|
||||
//Console.WriteLine($"图片序列将循环 {loopCount} 次以匹配音频时长");
|
||||
}
|
||||
|
||||
// --- 修正点 2: 重新构建 FFmpeg 参数列表 ---
|
||||
//var arguments = new List<string>
|
||||
//{
|
||||
// "-y", // 覆盖输出文件
|
||||
|
||||
// // --- 所有输入文件放在最前面 ---
|
||||
// // 图片序列输入
|
||||
// "-f", "image2",
|
||||
// "-r", imageFps.ToString(CultureInfo.InvariantCulture),
|
||||
// "-i", $"\"{imageSequencePattern}\"",
|
||||
|
||||
// // 音频输入
|
||||
// "-i", $"\"{audioFilePath}\"",
|
||||
|
||||
// // --- 修正点 3: 使用 filter_complex 对视频流进行循环 ---
|
||||
// // [0:v] 表示第一个输入(图片序列)的视频流
|
||||
// // loop={loopCount-1} 表示循环 (次数-1) 次
|
||||
// // [v] 是处理后的视频流的别名
|
||||
// "-filter_complex", $"\"[0:v]loop={loopCount - 1}[v]\"",
|
||||
|
||||
// // --- 修正点 4: 明确映射输出流 ---
|
||||
// // 将处理后的视频流 [v] 映射到输出
|
||||
// "-map", "\"[v]\"",
|
||||
// // 将第二个输入(音频文件)的音频流映射到输出
|
||||
// "-map", "\"1:a\"",
|
||||
|
||||
// // --- 视频编码配置 ---
|
||||
// "-c:v", VideoCodec,
|
||||
// "-preset", VideoPreset,
|
||||
// "-crf", $"{VideoCrf}",
|
||||
// "-s", $"{VideoWidth}x{VideoHeight}",
|
||||
// "-pix_fmt", "yuv420p",
|
||||
|
||||
// // --- 音频编码配置 ---
|
||||
// "-c:a", AudioCodec,
|
||||
// "-b:a", $"{AudioBitrate}",
|
||||
|
||||
// // --- 修正点 5: 使用 -shortest 参数 ---
|
||||
// // 确保视频和音频同时结束,即使循环次数计算得不完全精确
|
||||
// "-shortest",
|
||||
|
||||
// // --- 输出文件 ---
|
||||
// $"\"{outputVideoPath}\""
|
||||
//};
|
||||
|
||||
//AI优化后。。20251129
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"-y", // 覆盖输出文件
|
||||
|
||||
// 图片序列输入:恢复你原有的 -r,移除可能冲突的参数
|
||||
"-f", "image2",
|
||||
"-r", imageFps.ToString(CultureInfo.InvariantCulture), // 保留你原本的帧率参数
|
||||
"-start_number", "0", // 仅增加:避免序列编号问题(不影响原有逻辑)
|
||||
"-i", imageSequencePattern, // 关键修复:去掉引号(原代码的引号是核心失败原因)
|
||||
|
||||
// 音频输入:同样去掉引号
|
||||
"-i", audioFilePath,
|
||||
|
||||
// 滤镜修复:恢复简单写法,避免复杂参数(适配旧版 FFmpeg)
|
||||
"-filter_complex", $"[0:v]loop={loopCount - 1}[v]", // 去掉额外参数,和你原逻辑一致
|
||||
|
||||
// 流映射:去掉引号,恢复简单写法
|
||||
"-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
|
||||
};
|
||||
|
||||
string args = string.Join(" ", arguments);
|
||||
//Console.WriteLine($"执行FFmpeg命令: {_ffmpegExecutablePath} {args}");
|
||||
|
||||
// 执行命令
|
||||
await ExecuteFFmpegAsync(args, progress, cancellationToken);
|
||||
if (File.Exists(outputVideoPath))
|
||||
{
|
||||
return outputVideoPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("视频合成失败,未生成输出文件。");
|
||||
}
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 清理临时目录
|
||||
if (Directory.Exists(tempImageDir))
|
||||
{
|
||||
Directory.Delete(tempImageDir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行FFmpeg命令
|
||||
/// </summary>
|
||||
private async Task ExecuteFFmpegAsync(string arguments, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_ffmpegProcess != null && !_ffmpegProcess.HasExited)
|
||||
{
|
||||
throw new InvalidOperationException("已有一个FFmpeg进程正在运行。");
|
||||
}
|
||||
|
||||
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = _ffmpegExecutablePath,
|
||||
Arguments = arguments,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = System.Text.Encoding.UTF8,
|
||||
StandardErrorEncoding = System.Text.Encoding.UTF8
|
||||
};
|
||||
|
||||
_ffmpegProcess = new Process { StartInfo = startInfo };
|
||||
|
||||
_ffmpegProcess.ErrorDataReceived += (sender, e) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(e.Data)) return;
|
||||
//Console.WriteLine($"FFmpeg: {e.Data}");
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_ffmpegProcess.Start();
|
||||
_ffmpegProcess.BeginErrorReadLine();
|
||||
|
||||
using (_cancellationTokenSource.Token.Register(() =>
|
||||
{
|
||||
if (_ffmpegProcess != null && !_ffmpegProcess.HasExited)
|
||||
{
|
||||
try { _ffmpegProcess.Kill(); } catch { }
|
||||
}
|
||||
}))
|
||||
{
|
||||
await _ffmpegProcess.WaitForExitAsync(_cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
if (_cancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
throw new OperationCanceledException("FFmpeg进程被用户取消。", _cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
if (_ffmpegProcess.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"FFmpeg执行失败,退出码: {_ffmpegProcess.ExitCode}。请查看控制台输出获取详细错误信息。");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ffmpegProcess?.Dispose();
|
||||
_ffmpegProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_ffmpegProcess?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,5 +5,12 @@
|
||||
public static string DOWN_IMAGE_VIDEO_ENABLE="DOWN_IMGVIDEO";
|
||||
|
||||
public static string ASPNETCORE_URLS = "ASPNETCORE_URLS";
|
||||
|
||||
|
||||
public static string DY_FOLLOWEDS = "dy_followeds";
|
||||
public static string DY_COLLECTS = "dy_collects";
|
||||
public static string DY_FAVORITES = "dy_favorites";
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using dy.net.dto;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace dy.net.utils
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频标题模板生成器
|
||||
/// </summary>
|
||||
public static class VideoTitleGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据用户模板和原始数据生成最终标题
|
||||
/// </summary>
|
||||
/// <param name="template">用户设置的模板(如 "视频_{id}_{VideoTitle}_{ReleaseTime}")</param>
|
||||
/// <param name="data">标题所需的原始数据</param>
|
||||
/// <param name="timeFormat">时间字段格式化(默认:yyyy-MM-dd HH:mm:ss)</param>
|
||||
/// <param name="emptyPlaceholder">占位符对应数据为空时的替换值(默认:空字符串)</param>
|
||||
/// <returns>生成的最终标题</returns>
|
||||
/// <exception cref="ArgumentNullException">模板为空时抛出</exception>
|
||||
public static string Generate(
|
||||
string template,
|
||||
VideoTitleDataTemplate data,
|
||||
string timeFormat = "yyyyMMddHHmmss",
|
||||
string emptyPlaceholder = "")
|
||||
{
|
||||
// 校验入参
|
||||
if (string.IsNullOrWhiteSpace(template))
|
||||
throw new ArgumentNullException(nameof(template), "标题模板不能为空");
|
||||
data ??= new VideoTitleDataTemplate(); // 避免数据为空
|
||||
|
||||
// 1. 定义占位符与数据的映射关系(key:占位符名称,value:格式化后的值)
|
||||
var placeholderMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// 普通字段
|
||||
["Id"] = data.Id.ToString(),
|
||||
["VideoTitle"] = string.IsNullOrWhiteSpace(data.VideoTitle) ? emptyPlaceholder : data.VideoTitle,
|
||||
["FileHash"] = string.IsNullOrWhiteSpace(data.FileHash) ? emptyPlaceholder : data.FileHash,
|
||||
["Resolution"] = string.IsNullOrWhiteSpace(data.Resolution) ? emptyPlaceholder : data.Resolution,
|
||||
|
||||
// 时间字段(支持空值处理)
|
||||
//["SyncTime"] = data.SyncTime.HasValue ? data.SyncTime.Value.ToString(timeFormat) : emptyPlaceholder,
|
||||
["ReleaseTime"] = data.ReleaseTime.HasValue ? data.ReleaseTime.Value.ToString(timeFormat) : emptyPlaceholder,
|
||||
|
||||
// 文件大小(自动格式化:字节→KB/MB/GB,保留1位小数)
|
||||
//["FileSize"] = FormatFileSize(data.FileSize) ?? emptyPlaceholder
|
||||
};
|
||||
|
||||
// 2. 正则匹配模板中的占位符({占位符名称}),并替换
|
||||
var regex = new Regex(@"\{(?<key>[a-zA-Z0-9]+)\}", RegexOptions.Compiled);
|
||||
var finalTitle = regex.Replace(template, match =>
|
||||
{
|
||||
var placeholderKey = match.Groups["key"].Value;
|
||||
// 存在对应映射则替换,否则保留原占位符(避免替换错误)
|
||||
return placeholderMap.TryGetValue(placeholderKey, out var value) ? value : match.Value;
|
||||
});
|
||||
|
||||
return finalTitle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化文件大小(字节→KB/MB/GB)
|
||||
/// </summary>
|
||||
private static string FormatFileSize(long fileSizeInBytes)
|
||||
{
|
||||
if (fileSizeInBytes < 0) return "无效大小";
|
||||
if (fileSizeInBytes == 0) return "0B";
|
||||
|
||||
const long kb = 1024;
|
||||
const long mb = kb * 1024;
|
||||
const long gb = mb * 1024;
|
||||
|
||||
return fileSizeInBytes switch
|
||||
{
|
||||
< kb => $"{fileSizeInBytes}B",
|
||||
< mb => $"{fileSizeInBytes / (double)kb:F1}KB",
|
||||
< gb => $"{fileSizeInBytes / (double)mb:F1}MB",
|
||||
_ => $"{fileSizeInBytes / (double)gb:F1}GB"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace dy.net.utils
|
||||
{
|
||||
public class XBogus
|
||||
{
|
||||
private readonly int?[] _array;
|
||||
private readonly string _character;
|
||||
private readonly byte[] _uaKey = { 0x00, 0x01, 0x0c };
|
||||
private readonly string _userAgent;
|
||||
|
||||
public string Params { get; private set; }
|
||||
public string Xb { get; private set; }
|
||||
|
||||
public XBogus(string userAgent = "")
|
||||
{
|
||||
// 初始化 Array 数组(对应 Python 的 self.Array)
|
||||
_array = new int?[128];
|
||||
// 数字 0-9 对应 ASCII 48-57
|
||||
for (int i = 48; i <= 57; i++)
|
||||
_array[i] = i - 48;
|
||||
// 字母 A-F 对应 ASCII 65-70,映射为 10-15
|
||||
for (int i = 65; i <= 70; i++)
|
||||
_array[i] = i - 55;
|
||||
// 字母 a-f 对应 ASCII 97-102,映射为 10-15
|
||||
for (int i = 97; i <= 102; i++)
|
||||
_array[i] = i - 87;
|
||||
|
||||
// 字符映射表
|
||||
_character = "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=";
|
||||
|
||||
// 用户代理,默认值与 Python 一致
|
||||
_userAgent = string.IsNullOrEmpty(userAgent)
|
||||
? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0"
|
||||
: userAgent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将字符串通过 MD5 哈希转换为整数数组
|
||||
/// </summary>
|
||||
private int[] Md5StrToArray(string md5Str)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(md5Str) && md5Str.Length > 32)
|
||||
return md5Str.Select(c => (int)c).ToArray();
|
||||
|
||||
var result = new List<int>();
|
||||
for (int i = 0; i < md5Str.Length; i += 2)
|
||||
{
|
||||
if (i + 1 >= md5Str.Length)
|
||||
break;
|
||||
|
||||
int? high = _array[md5Str[i]];
|
||||
int? low = _array[md5Str[i + 1]];
|
||||
if (high == null || low == null)
|
||||
result.Add(0);
|
||||
else
|
||||
result.Add(((int)high << 4) | (int)low);
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多轮 MD5 哈希加密 URL 参数
|
||||
/// </summary>
|
||||
private int[] Md5Encrypt(string urlParams)
|
||||
{
|
||||
string firstMd5 = Md5(urlParams);
|
||||
int[] firstArray = Md5StrToArray(firstMd5);
|
||||
string secondMd5 = Md5(firstArray);
|
||||
return Md5StrToArray(secondMd5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 MD5 哈希值
|
||||
/// </summary>
|
||||
private string Md5(object input)
|
||||
{
|
||||
int[] dataArray;
|
||||
switch (input)
|
||||
{
|
||||
case string str:
|
||||
dataArray = Md5StrToArray(str);
|
||||
break;
|
||||
case int[] arr:
|
||||
dataArray = arr;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Invalid input type. Expected string or int array.");
|
||||
}
|
||||
|
||||
using (var md5 = MD5.Create())
|
||||
{
|
||||
byte[] bytes = dataArray.Select(i => (byte)(i & 0xFF)).ToArray();
|
||||
byte[] hashBytes = md5.ComputeHash(bytes);
|
||||
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 第一次编码转换
|
||||
/// </summary>
|
||||
private string EncodingConversion(
|
||||
int a, int b, int c, int e, int d, int t, int f, int r, int n, int o,
|
||||
int i, int _, int x, int u, int s, int l, int v, int h, int p)
|
||||
{
|
||||
var bytes = new byte[]
|
||||
{
|
||||
(byte)a, (byte)i, (byte)b, (byte)_ , (byte)c, (byte)x,
|
||||
(byte)e, (byte)u, (byte)d, (byte)s, (byte)t, (byte)l,
|
||||
(byte)f, (byte)v, (byte)r, (byte)h, (byte)n, (byte)p, (byte)o
|
||||
};
|
||||
return Encoding.GetEncoding("ISO-8859-1").GetString(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 第二次编码转换
|
||||
/// </summary>
|
||||
private string EncodingConversion2(int a, int b, string c)
|
||||
{
|
||||
return ((char)a).ToString() + ((char)b).ToString() + c;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RC4 加密算法
|
||||
/// </summary>
|
||||
private byte[] Rc4Encrypt(byte[] key, byte[] data)
|
||||
{
|
||||
int[] S = Enumerable.Range(0, 256).ToArray();
|
||||
int j = 0;
|
||||
|
||||
// 初始化 S 盒
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
j = (j + S[i] + key[i % key.Length]) % 256;
|
||||
(S[i], S[j]) = (S[j], S[i]);
|
||||
}
|
||||
|
||||
// 生成密文
|
||||
var encrypted = new byte[data.Length];
|
||||
int i2 = 0, j2 = 0;
|
||||
for (int k = 0; k < data.Length; k++)
|
||||
{
|
||||
i2 = (i2 + 1) % 256;
|
||||
j2 = (j2 + S[i2]) % 256;
|
||||
(S[i2], S[j2]) = (S[j2], S[i2]);
|
||||
int t = (S[i2] + S[j2]) % 256;
|
||||
encrypted[k] = (byte)(data[k] ^ S[t]);
|
||||
}
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 位运算计算
|
||||
/// </summary>
|
||||
private string Calculation(int a1, int a2, int a3)
|
||||
{
|
||||
int x1 = (a1 & 0xFF) << 16;
|
||||
int x2 = (a2 & 0xFF) << 8;
|
||||
int x3 = x1 | x2 | (a3 & 0xFF);
|
||||
|
||||
char c1 = _character[(x3 & 0x0FC0000) >> 18]; // 16515072 = 0x0FC0000
|
||||
char c2 = _character[(x3 & 0x003F000) >> 12]; // 258048 = 0x003F000
|
||||
char c3 = _character[(x3 & 0x0000FC0) >> 6]; // 4032 = 0x0000FC0
|
||||
char c4 = _character[x3 & 0x3F];
|
||||
|
||||
return $"{c1}{c2}{c3}{c4}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 X-Bogus 值
|
||||
/// </summary>
|
||||
public (string Params, string Xb, string UserAgent) GetXBogus(string urlParams)
|
||||
{
|
||||
// 计算 array1
|
||||
byte[] uaBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(_userAgent);
|
||||
byte[] rc4Ua = Rc4Encrypt(_uaKey, uaBytes);
|
||||
string base64Ua = Convert.ToBase64String(rc4Ua);
|
||||
string md5Ua = Md5(base64Ua);
|
||||
int[] array1 = Md5StrToArray(md5Ua);
|
||||
|
||||
// 计算 array2(固定 MD5:d41d8cd98f00b204e9800998ecf8427e 是空字符串的 MD5)
|
||||
int[] array2 = Md5StrToArray(Md5(Md5StrToArray("d41d8cd98f00b204e9800998ecf8427e")));
|
||||
|
||||
// 计算 URL 参数的 MD5 数组
|
||||
int[] urlParamsArray = Md5Encrypt(urlParams);
|
||||
|
||||
// 时间戳和固定值
|
||||
long timer = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
int ct = 536919696;
|
||||
|
||||
// 构建 new_array
|
||||
var newArray = new List<double>
|
||||
{
|
||||
64, 0.00390625, 1, 12,
|
||||
urlParamsArray.Length > 14 ? urlParamsArray[14] : 0,
|
||||
urlParamsArray.Length > 15 ? urlParamsArray[15] : 0,
|
||||
array2.Length > 14 ? array2[14] : 0,
|
||||
array2.Length > 15 ? array2[15] : 0,
|
||||
array1.Length > 14 ? array1[14] : 0,
|
||||
array1.Length > 15 ? array1[15] : 0,
|
||||
(timer >> 24) & 0xFF,
|
||||
(timer >> 16) & 0xFF,
|
||||
(timer >> 8) & 0xFF,
|
||||
timer & 0xFF,
|
||||
(ct >> 24) & 0xFF,
|
||||
(ct >> 16) & 0xFF,
|
||||
(ct >> 8) & 0xFF,
|
||||
ct & 0xFF
|
||||
};
|
||||
|
||||
// 计算异或结果
|
||||
int xorResult = (int)newArray[0];
|
||||
for (int i = 1; i < newArray.Count; i++)
|
||||
{
|
||||
int b = (int)newArray[i];
|
||||
xorResult ^= b;
|
||||
}
|
||||
newArray.Add(xorResult);
|
||||
|
||||
// 拆分 array3 和 array4
|
||||
var array3 = new List<int>();
|
||||
var array4 = new List<int>();
|
||||
for (int i = 0; i < newArray.Count; i++)
|
||||
{
|
||||
array3.Add((int)newArray[i]);
|
||||
if (i + 1 < newArray.Count)
|
||||
array4.Add((int)newArray[i + 1]);
|
||||
i++;
|
||||
}
|
||||
|
||||
// 合并数组
|
||||
int[] mergeArray = array3.Concat(array4).ToArray();
|
||||
|
||||
// 生成乱码
|
||||
string encoding1 = EncodingConversion(
|
||||
mergeArray[0], mergeArray[1], mergeArray[2], mergeArray[3], mergeArray[4],
|
||||
mergeArray[5], mergeArray[6], mergeArray[7], mergeArray[8], mergeArray[9],
|
||||
mergeArray[10], mergeArray[11], mergeArray[12], mergeArray[13], mergeArray[14],
|
||||
mergeArray[15], mergeArray[16], mergeArray[17], mergeArray[18]
|
||||
);
|
||||
|
||||
byte[] encoding1Bytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(encoding1);
|
||||
byte[] rc4Key = Encoding.GetEncoding("ISO-8859-1").GetBytes("ÿ");
|
||||
byte[] rc4Encrypted = Rc4Encrypt(rc4Key, encoding1Bytes);
|
||||
string rc4Str = Encoding.GetEncoding("ISO-8859-1").GetString(rc4Encrypted);
|
||||
|
||||
string garbledCode = EncodingConversion2(2, 255, rc4Str);
|
||||
|
||||
// 计算 X-Bogus
|
||||
StringBuilder xbBuilder = new StringBuilder();
|
||||
for (int i = 0; i < garbledCode.Length; i += 3)
|
||||
{
|
||||
if (i + 2 >= garbledCode.Length)
|
||||
break;
|
||||
|
||||
int a = garbledCode[i];
|
||||
int b = garbledCode[i + 1];
|
||||
int c = garbledCode[i + 2];
|
||||
xbBuilder.Append(Calculation(a, b, c));
|
||||
}
|
||||
|
||||
// 结果赋值
|
||||
Xb = xbBuilder.ToString();
|
||||
Params = $"{urlParams}&X-Bogus={Xb}";
|
||||
|
||||
return (Params, Xb, _userAgent);
|
||||
}
|
||||
}
|
||||
|
||||
// 测试代码
|
||||
//public class XBogusTest
|
||||
//{
|
||||
// public static void Main()
|
||||
// {
|
||||
// string ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
|
||||
// var xb = new XBogus(ua);
|
||||
|
||||
// string dyUrlParams = "device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=MS4wLjABAAAAW9FWcqS7RdQAWPd2AA5fL_ilmqsIFUCQ_Iym6Yh9_cUa6ZRqVLjVQSUjlHrfXY1Y&max_cursor=0&locate_query=false&show_live_replay_strategy=1&need_time_list=1&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&pc_client_type=1&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=122.0.0.0&browser_online=true&engine_name=Blink&engine_version=122.0.0.0&os_name=Windows&os_version=10&cpu_core_num=12&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7335414539335222835&msToken=p9Y7fUBuq9DKvAuN27Peml6JbaMqG2ZcXfFiyDv1jcHrCN00uidYqUgSuLsKl1onC-E_n82m-aKKYE0QGEmxIWZx9iueQ6WLbvzPfqnMk4GBAlQIHcDzxb38FLXXQxAm";
|
||||
// string tkUrlParams = "WebIdLastTime=1713796127&abTestVersion=%5Bobject%20Object%5D&aid=1988&appType=t&app_language=zh-Hans&app_name=tiktok_web&browser_name=Mozilla&browser_online=true&browser_platform=Win32&browser_version=5.0%20%28Windows%20NT%2010.0%3B%20Win64%3B%20x64%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome%2F123.0.0.0%20Safari%2F537.36&channel=tiktok_web&device_id=7360698239018452498&odinId=7360698115047851026®ion=TW&tz_name=Asia%2FHong_Kong&uniqueId=rei_toy625";
|
||||
|
||||
// var dyResult = xb.GetXBogus(dyUrlParams);
|
||||
// Console.WriteLine($"Douyin - URL: {dyResult.Params}, X-Bogus: {dyResult.Xb}, UA: {dyResult.UserAgent}");
|
||||
|
||||
// var tkResult = xb.GetXBogus(tkUrlParams);
|
||||
// Console.WriteLine($"TikTok - URL: {tkResult.Params}, X-Bogus: {tkResult.Xb}, UA: {tkResult.UserAgent}");
|
||||
// }
|
||||
//}
|
||||
}
|
||||
Reference in New Issue
Block a user