Files
douyin/service/DouyinLiveStatusClient.cs

211 lines
8.7 KiB
C#
Raw Permalink 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.Net;
using dy.net.model.dto;
using dy.net.utils;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace dy.net.service
{
public interface IDouyinLiveStatusClient
{
Task<DouyinLiveStatusProbe> ProbeAsync(
string secUid,
string cookie,
CancellationToken cancellationToken = default);
}
public sealed class DouyinLiveStatusRequestException : InvalidOperationException
{
public DouyinLiveStatusRequestException(
string message,
HttpStatusCode? statusCode = null,
bool requiresAccountCooldown = false,
Exception innerException = null)
: base(message, innerException)
{
StatusCode = statusCode;
RequiresAccountCooldown = requiresAccountCooldown;
}
public HttpStatusCode? StatusCode { get; }
public bool RequiresAccountCooldown { get; }
}
public sealed class DouyinLiveStatusClient : IDouyinLiveStatusClient
{
private const string ProfilePath = "/aweme/v1/web/user/profile/other/";
private readonly IHttpClientFactory _clientFactory;
private readonly DouyinABogusSigner _signer = new();
public DouyinLiveStatusClient(IHttpClientFactory clientFactory) => _clientFactory = clientFactory;
public async Task<DouyinLiveStatusProbe> ProbeAsync(
string secUid,
string cookie,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(secUid))
throw new InvalidOperationException("博主 SecUid 为空,无法检查直播状态");
if (string.IsNullOrWhiteSpace(cookie))
throw new InvalidOperationException("授权 Cookie 为空,无法检查直播状态");
var parameters = BuildParameters(secUid.Trim(), cookie);
var unsignedQuery = BuildQueryString(parameters);
var requestUri = ProfilePath + "?" + unsignedQuery + "&a_bogus=" + Encode(_signer.Sign(unsignedQuery));
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Referrer = new Uri("https://www.douyin.com/user/" + Encode(secUid.Trim()));
request.Headers.TryAddWithoutValidation("Accept", "application/json, text/plain, */*");
request.Headers.TryAddWithoutValidation("Cookie", cookie);
var client = _clientFactory.CreateClient(DouyinRequestParamManager.DY_HTTP_CLIENT);
try
{
using var response = await client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
if (!response.IsSuccessStatusCode)
{
var shouldCooldown = response.StatusCode is HttpStatusCode.Forbidden
or HttpStatusCode.TooManyRequests
or HttpStatusCode.Unauthorized;
throw new DouyinLiveStatusRequestException(
$"抖音直播状态请求失败(HTTP {(int)response.StatusCode}",
response.StatusCode,
shouldCooldown);
}
var content = await response.Content.ReadAsStringAsync(cancellationToken);
UserProfileResponse payload;
try
{
payload = JsonConvert.DeserializeObject<UserProfileResponse>(content);
}
catch (JsonException ex)
{
throw new DouyinLiveStatusRequestException(
"抖音返回了无法识别的直播状态响应,可能需要验证授权",
requiresAccountCooldown: true,
innerException: ex);
}
if (payload?.User == null)
throw new DouyinLiveStatusRequestException(
"抖音没有返回博主资料,可能需要验证授权",
requiresAccountCooldown: true);
if (payload.StatusCode != 0)
throw new DouyinLiveStatusRequestException(
$"抖音拒绝了直播状态查询(状态码 {payload.StatusCode}",
requiresAccountCooldown: true);
return DouyinLiveStatusParser.Parse(
payload.User.LiveStatus,
payload.User.RoomId,
payload.User.RoomIdStr,
payload.User.RoomData);
}
catch (DouyinLiveStatusRequestException)
{
throw;
}
catch (HttpRequestException ex)
{
throw new DouyinLiveStatusRequestException("抖音直播状态网络请求失败,请稍后重试", innerException: ex);
}
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
throw new DouyinLiveStatusRequestException("抖音直播状态查询超时,请稍后重试", innerException: ex);
}
}
private static List<KeyValuePair<string, string>> BuildParameters(string secUid, string cookie)
{
var cookies = ParseCookie(cookie);
return new List<KeyValuePair<string, string>>
{
new("device_platform", "webapp"),
new("aid", "6383"),
new("channel", "channel_pc_web"),
new("update_version_code", "170400"),
new("pc_client_type", "1"),
new("pc_libra_divert", "Windows"),
new("support_h265", "1"),
new("support_dash", "1"),
new("version_code", "170400"),
new("version_name", "17.4.0"),
new("cookie_enabled", "true"),
new("screen_width", "1536"),
new("screen_height", "864"),
new("browser_language", "zh-CN"),
new("browser_platform", "Win32"),
new("browser_name", "Chrome"),
new("browser_version", "119.0.0.0"),
new("browser_online", "true"),
new("engine_name", "Blink"),
new("engine_version", "119.0.0.0"),
new("os_name", "Windows"),
new("os_version", "10"),
new("cpu_core_num", "16"),
new("device_memory", "8"),
new("platform", "PC"),
new("downlink", "10"),
new("effective_type", "4g"),
new("round_trip_time", "200"),
new("uifid", CookieValue(cookies, "UIFID", "UIFID_TEMP")),
new("msToken", CookieValue(cookies, "msToken")),
new("sec_user_id", secUid),
new("publish_video_strategy_type", "2"),
new("personal_center_strategy", "1")
};
}
private static Dictionary<string, string> ParseCookie(string cookie)
{
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var part in (cookie ?? string.Empty).Split(';', StringSplitOptions.RemoveEmptyEntries))
{
var separator = part.IndexOf('=');
if (separator <= 0) continue;
values[part[..separator].Trim()] = part[(separator + 1)..].Trim();
}
return values;
}
private static string CookieValue(Dictionary<string, string> cookies, params string[] names)
{
foreach (var name in names)
if (cookies.TryGetValue(name, out var value)) return value;
return string.Empty;
}
private static string BuildQueryString(IEnumerable<KeyValuePair<string, string>> parameters) =>
string.Join("&", parameters.Select(x => Encode(x.Key) + "=" + Encode(x.Value ?? string.Empty)));
private static string Encode(string value) => Uri.EscapeDataString(value ?? string.Empty);
private sealed class UserProfileResponse
{
[JsonProperty("status_code")]
public int StatusCode { get; set; }
[JsonProperty("user")]
public UserProfile User { get; set; }
}
private sealed class UserProfile
{
[JsonProperty("live_status")]
public int? LiveStatus { get; set; }
[JsonProperty("room_id")]
public string RoomId { get; set; }
[JsonProperty("room_id_str")]
public string RoomIdStr { get; set; }
[JsonProperty("room_data")]
public JToken RoomData { get; set; }
}
}
}