提交功能
This commit is contained in:
296
VideoDownload/DouyinShareRouterData.cs
Normal file
296
VideoDownload/DouyinShareRouterData.cs
Normal file
@@ -0,0 +1,296 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ACBuildService;
|
||||
|
||||
/// <summary>
|
||||
/// 抖音 分享文本中的视频数据
|
||||
/// </summary>
|
||||
public class DouYinShareRouterData
|
||||
{
|
||||
[JsonProperty("loaderData")] public LoaderData LoaderData { get; set; }
|
||||
}
|
||||
|
||||
public class LoaderData
|
||||
{
|
||||
[JsonProperty("video_(id)/page")] public VideoIdPage VideoIdPage { get; set; }
|
||||
}
|
||||
|
||||
public class VideoIdPage
|
||||
{
|
||||
[JsonProperty("isAutoOpenApp")] public bool IsAutoOpenApp { get; set; }
|
||||
|
||||
[JsonProperty("appName")] public string AppName { get; set; }
|
||||
|
||||
[JsonProperty("query")] public Query Query { get; set; }
|
||||
|
||||
[JsonProperty("isSpider")] public bool IsSpider { get; set; }
|
||||
|
||||
[JsonProperty("ua")] public string Ua { get; set; }
|
||||
|
||||
[JsonProperty("isVideoOptimize")] public bool IsVideoOptimize { get; set; }
|
||||
|
||||
[JsonProperty("commonContext")] public CommonContext CommonContext { get; set; }
|
||||
|
||||
[JsonProperty("itemId")] public string ItemId { get; set; }
|
||||
|
||||
[JsonProperty("webId")] public string WebId { get; set; }
|
||||
|
||||
[JsonProperty("renderInSSR")] public long RenderInSsr { get; set; }
|
||||
|
||||
[JsonProperty("host")] public string Host { get; set; }
|
||||
|
||||
[JsonProperty("videoInfoRes")] public VideoInfoRes VideoInfoRes { get; set; }
|
||||
|
||||
[JsonProperty("darkModeAdaptation")] public bool DarkModeAdaptation { get; set; }
|
||||
|
||||
[JsonProperty("lastPath")] public string LastPath { get; set; }
|
||||
|
||||
[JsonProperty("isNotSupportWebp")] public bool IsNotSupportWebp { get; set; }
|
||||
|
||||
[JsonProperty("serverToken")] public string ServerToken { get; set; }
|
||||
}
|
||||
|
||||
public class CommonContext
|
||||
{
|
||||
[JsonProperty("webId")] public string WebId { get; set; }
|
||||
|
||||
[JsonProperty("renderInSSR")] public long RenderInSsr { get; set; }
|
||||
|
||||
[JsonProperty("appName")] public string AppName { get; set; }
|
||||
|
||||
[JsonProperty("query")] public Query Query { get; set; }
|
||||
|
||||
[JsonProperty("host")] public string Host { get; set; }
|
||||
|
||||
[JsonProperty("isSpider")] public bool IsSpider { get; set; }
|
||||
|
||||
[JsonProperty("lastPath")] public string LastPath { get; set; }
|
||||
|
||||
[JsonProperty("isNotSupportWebp")] public bool IsNotSupportWebp { get; set; }
|
||||
|
||||
[JsonProperty("ua")] public string Ua { get; set; }
|
||||
}
|
||||
|
||||
public class Query
|
||||
{
|
||||
[JsonProperty("titleType")] public string TitleType { get; set; }
|
||||
|
||||
[JsonProperty("iid")] public string Iid { get; set; }
|
||||
|
||||
[JsonProperty("u_code")] public string UCode { get; set; }
|
||||
|
||||
[JsonProperty("from_ssr")] public long FromSsr { get; set; }
|
||||
|
||||
[JsonProperty("mid")] public string Mid { get; set; }
|
||||
|
||||
[JsonProperty("share_sign")] public string ShareSign { get; set; }
|
||||
|
||||
[JsonProperty("with_sec_did")] public long WithSecDid { get; set; }
|
||||
|
||||
[JsonProperty("from_aid")] public long FromAid { get; set; }
|
||||
|
||||
[JsonProperty("from")] public string From { get; set; }
|
||||
|
||||
[JsonProperty("region")] public string Region { get; set; }
|
||||
|
||||
[JsonProperty("did")] public string Did { get; set; }
|
||||
|
||||
[JsonProperty("share_version")] public long ShareVersion { get; set; }
|
||||
|
||||
[JsonProperty("ts")] public long Ts { get; set; }
|
||||
}
|
||||
|
||||
public class VideoInfoRes
|
||||
{
|
||||
[JsonProperty("status_code")] public long StatusCode { get; set; }
|
||||
|
||||
[JsonProperty("filter_list")] public List<object> FilterList { get; set; }
|
||||
|
||||
[JsonProperty("item_list")] public List<ItemList> ItemList { get; set; }
|
||||
|
||||
[JsonProperty("extra")] public Extra Extra { get; set; }
|
||||
}
|
||||
|
||||
public class Extra
|
||||
{
|
||||
[JsonProperty("now")] public long Now { get; set; }
|
||||
}
|
||||
|
||||
public class ItemList
|
||||
{
|
||||
[JsonProperty("video")] public Video Video { get; set; }
|
||||
|
||||
[JsonProperty("music")] public Music Music { get; set; }
|
||||
|
||||
[JsonProperty("create_time")] public long CreateTime { get; set; }
|
||||
|
||||
[JsonProperty("author")] public Author Author { get; set; }
|
||||
|
||||
[JsonProperty("risk_infos")] public RiskInfos RiskInfos { get; set; }
|
||||
|
||||
[JsonProperty("mix_info")] public MixInfo MixInfo { get; set; }
|
||||
|
||||
[JsonProperty("aweme_id")] public string AwemeId { get; set; }
|
||||
|
||||
[JsonProperty("group_id_str")] public string GroupIdStr { get; set; }
|
||||
|
||||
[JsonProperty("text_extra")] public List<TextExtra> TextExtra { get; set; }
|
||||
|
||||
[JsonProperty("aweme_type")] public long AwemeType { get; set; }
|
||||
|
||||
[JsonProperty("desc")] public string Desc { get; set; }
|
||||
|
||||
[JsonProperty("statistics")] public Statistics Statistics { get; set; }
|
||||
}
|
||||
|
||||
public class Author
|
||||
{
|
||||
[JsonProperty("avatar_medium")] public AvatarMedium AvatarMedium { get; set; }
|
||||
|
||||
[JsonProperty("unique_id")] public string UniqueId { get; set; }
|
||||
|
||||
[JsonProperty("sec_uid")] public string SecUid { get; set; }
|
||||
|
||||
[JsonProperty("signature")] public string Signature { get; set; }
|
||||
|
||||
[JsonProperty("mplatform_followers_count")]
|
||||
public long MplatformFollowersCount { get; set; }
|
||||
|
||||
[JsonProperty("avatar_thumb")] public AvatarMedium AvatarThumb { get; set; }
|
||||
|
||||
[JsonProperty("follow_status")] public long FollowStatus { get; set; }
|
||||
|
||||
[JsonProperty("short_id")] public string ShortId { get; set; }
|
||||
|
||||
[JsonProperty("following_count")] public long FollowingCount { get; set; }
|
||||
|
||||
[JsonProperty("nickname")] public string Nickname { get; set; }
|
||||
|
||||
[JsonProperty("favoriting_count")] public long FavoritingCount { get; set; }
|
||||
}
|
||||
|
||||
public class AvatarMedium
|
||||
{
|
||||
[JsonProperty("url_list")] public List<Uri> UrlList { get; set; }
|
||||
|
||||
[JsonProperty("uri")] public string Uri { get; set; }
|
||||
}
|
||||
|
||||
public class MixInfo
|
||||
{
|
||||
[JsonProperty("mix_id")] public string MixId { get; set; }
|
||||
|
||||
[JsonProperty("cover_url")] public AvatarMedium CoverUrl { get; set; }
|
||||
|
||||
[JsonProperty("next_info")] public NextInfo NextInfo { get; set; }
|
||||
|
||||
[JsonProperty("statis")] public Statis Statis { get; set; }
|
||||
|
||||
[JsonProperty("create_time")] public long CreateTime { get; set; }
|
||||
|
||||
[JsonProperty("extra")] public string Extra { get; set; }
|
||||
|
||||
[JsonProperty("mix_name")] public string MixName { get; set; }
|
||||
|
||||
[JsonProperty("status")] public Status Status { get; set; }
|
||||
|
||||
[JsonProperty("desc")] public string Desc { get; set; }
|
||||
}
|
||||
|
||||
public class NextInfo
|
||||
{
|
||||
[JsonProperty("cover_url")] public AvatarMedium CoverUrl { get; set; }
|
||||
|
||||
[JsonProperty("mix_name")] public string MixName { get; set; }
|
||||
|
||||
[JsonProperty("desc")] public string Desc { get; set; }
|
||||
}
|
||||
|
||||
public class Statis
|
||||
{
|
||||
[JsonProperty("collect_vv")] public long CollectVv { get; set; }
|
||||
|
||||
[JsonProperty("current_episode")] public long CurrentEpisode { get; set; }
|
||||
|
||||
[JsonProperty("play_vv")] public long PlayVv { get; set; }
|
||||
|
||||
[JsonProperty("updated_to_episode")] public long UpdatedToEpisode { get; set; }
|
||||
}
|
||||
|
||||
public class Status
|
||||
{
|
||||
[JsonProperty("is_collected")] public long IsCollected { get; set; }
|
||||
|
||||
[JsonProperty("status")] public long StatusStatus { get; set; }
|
||||
}
|
||||
|
||||
public class Music
|
||||
{
|
||||
[JsonProperty("duration")] public long Duration { get; set; }
|
||||
|
||||
[JsonProperty("author")] public string Author { get; set; }
|
||||
|
||||
[JsonProperty("cover_medium")] public AvatarMedium CoverMedium { get; set; }
|
||||
|
||||
[JsonProperty("mid")] public string Mid { get; set; }
|
||||
|
||||
[JsonProperty("title")] public string Title { get; set; }
|
||||
|
||||
[JsonProperty("cover_hd")] public AvatarMedium CoverHd { get; set; }
|
||||
|
||||
[JsonProperty("cover_large")] public AvatarMedium CoverLarge { get; set; }
|
||||
|
||||
[JsonProperty("cover_thumb")] public AvatarMedium CoverThumb { get; set; }
|
||||
|
||||
[JsonProperty("status")] public long Status { get; set; }
|
||||
}
|
||||
|
||||
public class RiskInfos
|
||||
{
|
||||
[JsonProperty("warn")] public bool Warn { get; set; }
|
||||
|
||||
[JsonProperty("type")] public long Type { get; set; }
|
||||
|
||||
[JsonProperty("content")] public string Content { get; set; }
|
||||
|
||||
[JsonProperty("reflow_unplayable")] public long ReflowUnplayable { get; set; }
|
||||
}
|
||||
|
||||
public class Statistics
|
||||
{
|
||||
[JsonProperty("comment_count")] public long CommentCount { get; set; }
|
||||
|
||||
[JsonProperty("share_count")] public long ShareCount { get; set; }
|
||||
|
||||
[JsonProperty("aweme_id")] public string AwemeId { get; set; }
|
||||
|
||||
[JsonProperty("digg_count")] public long DiggCount { get; set; }
|
||||
|
||||
[JsonProperty("play_count")] public long PlayCount { get; set; }
|
||||
|
||||
[JsonProperty("collect_count")] public long CollectCount { get; set; }
|
||||
}
|
||||
|
||||
public class TextExtra
|
||||
{
|
||||
[JsonProperty("hashtag_id")] public long HashtagId { get; set; }
|
||||
|
||||
[JsonProperty("start")] public long Start { get; set; }
|
||||
|
||||
[JsonProperty("end")] public long End { get; set; }
|
||||
|
||||
[JsonProperty("type")] public long Type { get; set; }
|
||||
|
||||
[JsonProperty("hashtag_name")] public string HashtagName { get; set; }
|
||||
}
|
||||
|
||||
public class Video
|
||||
{
|
||||
[JsonProperty("cover")] public AvatarMedium Cover { get; set; }
|
||||
|
||||
[JsonProperty("play_addr")] public AvatarMedium PlayAddr { get; set; }
|
||||
|
||||
[JsonProperty("width")] public long Width { get; set; }
|
||||
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
}
|
||||
177
VideoDownload/Platforms/DouYin.cs
Normal file
177
VideoDownload/Platforms/DouYin.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text.RegularExpressions;
|
||||
using Downloader;
|
||||
using Newtonsoft.Json;
|
||||
using RestSharp;
|
||||
|
||||
namespace ACBuildService;
|
||||
|
||||
public class DouYin : DownloadPlatforms
|
||||
{
|
||||
private static readonly Dictionary<string, string> _defaultHeaders = new()
|
||||
{
|
||||
{
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36"
|
||||
},
|
||||
{ "sec-fetch-site", "same-origin" },
|
||||
{ "sec-fetch-mode", "cors" },
|
||||
{ "sec-fetch-dest", "empty" },
|
||||
{ "sec-ch-ua-platform", "Windows" },
|
||||
{ "sec-ch-ua-mobile", "?0" },
|
||||
{ "sec-ch-ua", "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\"" },
|
||||
{ "referer", "https://www.douyin.com/?recommend=1" },
|
||||
{ "priority", "u=1, i" },
|
||||
{ "pragma", "no-cache" },
|
||||
{ "cache-control", "no-cache" },
|
||||
{ "accept-language", "zh-CN,zh;q=0.9,en;q=0.8" },
|
||||
{
|
||||
"accept",
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
|
||||
},
|
||||
{ "dnt", "1" }
|
||||
};
|
||||
|
||||
public async override Task<string> ExtractUrlAsync(string text)
|
||||
{
|
||||
Log.Info($"开始解析抖音链接 {text}");
|
||||
return Regex.Match(text, @"https?://[^\s]+").Value;
|
||||
}
|
||||
|
||||
public override async Task<VideoModel> ParseShare(string DownloadUrlText)
|
||||
{
|
||||
if (!DownloadUrlText.Contains("https://"))
|
||||
{
|
||||
Log.Error("请输入正确的分享链接");
|
||||
return null;
|
||||
}
|
||||
|
||||
var downloadUrl = await ExtractUrlAsync(DownloadUrlText);
|
||||
VideoModel Data = await ExtractVideoDataAsync(downloadUrl);
|
||||
Data.ShareId = downloadUrl.Replace("https://v.douyin.com/", "").Replace("/", "");
|
||||
|
||||
return Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据链接 https://v.douyin.com/ircqoExo/ 解析出页面中的数据
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <returns></returns>
|
||||
public override Task<VideoModel> ExtractVideoDataAsync(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Info($"开始解析链接 {url}");
|
||||
// 创建RestClient
|
||||
RestClient client = new(url);
|
||||
|
||||
// 创建请求对象
|
||||
RestRequest request = new();
|
||||
|
||||
// 设置 User-Agent 模拟手机浏览器
|
||||
request.AddHeaders(_defaultHeaders);
|
||||
|
||||
// 发送请求并获取响应
|
||||
var response = client.Execute(request);
|
||||
if (!response.IsSuccessful) throw new HttpRequestException("request is fail");
|
||||
|
||||
var content = response.Content;
|
||||
Log.Info($"开始解析响应内容 {content}");
|
||||
if (content is null) throw new InvalidDataException("content is null");
|
||||
|
||||
const string routerDataPattern = @"_ROUTER_DATA\s*=\s*(\{.*?\})<";
|
||||
|
||||
var matchJson = Regex.Match(content, routerDataPattern);
|
||||
|
||||
Log.Info($"开始解析匹配到的json {matchJson}");
|
||||
if (matchJson.Groups.Count < 2) throw new InvalidDataException("未匹配到合法的数据,matchJson.Groups.Count < 2");
|
||||
|
||||
var videoJson = matchJson.Groups[1].Value;
|
||||
Log.Info($"开始解析匹配到的json {videoJson}");
|
||||
// 反序列化JSON字符串为C#对象
|
||||
var videoData = JsonConvert.DeserializeObject<DouYinShareRouterData>(videoJson);
|
||||
|
||||
if (videoData is null) throw new InvalidDataException("JSON解析数据为空,请检查分享链接是否正确,如有更多问题请查看日志");
|
||||
|
||||
var videoInfoData = videoData.LoaderData.VideoIdPage.VideoInfoRes.ItemList.First();
|
||||
|
||||
var video = new VideoModel
|
||||
{
|
||||
Platform = ShortVideoPlatformEnum.DouYin,
|
||||
VideoId = videoInfoData.AwemeId,
|
||||
AuthorName = videoInfoData.Author.Nickname,
|
||||
UniqueId = videoInfoData.Author.UniqueId == ""
|
||||
? videoInfoData.Author.ShortId
|
||||
: videoInfoData.Author.UniqueId,
|
||||
AuthorAvatar = videoInfoData.Author.AvatarThumb.UrlList.First().ToString(),
|
||||
Title = videoInfoData.Author.Signature,
|
||||
Cover = videoInfoData.Video.Cover.UrlList.Last().ToString(),
|
||||
Mp3Url = "",
|
||||
CreatedTime =
|
||||
DateTimeOffset.FromUnixTimeSeconds(videoInfoData.CreateTime)
|
||||
.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
Desc = videoInfoData.Desc,
|
||||
Duration = "",
|
||||
DiggCount = videoInfoData.Statistics.DiggCount,
|
||||
CollectCount = videoInfoData.Statistics.CollectCount,
|
||||
CommentCount = videoInfoData.Statistics.CommentCount,
|
||||
ShareCount = videoInfoData.Statistics.ShareCount
|
||||
};
|
||||
switch (videoInfoData.AwemeType)
|
||||
{
|
||||
case 2:
|
||||
video.VideoUrl = videoInfoData.Video.Cover.UrlList.First().ToString();
|
||||
break;
|
||||
default:
|
||||
video.VideoUrl = videoInfoData.Video.PlayAddr.UrlList.First().ToString().Replace("playwm", "play");
|
||||
break;
|
||||
}
|
||||
|
||||
if (video.VideoUrl.Contains("ratio=720p"))
|
||||
{
|
||||
video.VideoUrl = video.VideoUrl.Replace("ratio=720p", "ratio=1080p");
|
||||
}
|
||||
|
||||
return Task.FromResult(video);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<bool> DownloadAsync(string url, string savePath, string fileName,
|
||||
EventHandler<DownloadProgressChangedEventArgs> onProgressChanged,
|
||||
EventHandler<AsyncCompletedEventArgs> onProgressCompleted)
|
||||
{
|
||||
DownloadConfiguration downloadConfiguration = new()
|
||||
{
|
||||
ChunkCount = 8, // Download in 8 chunks (increase for larger files)
|
||||
MaxTryAgainOnFailure = 5,
|
||||
Timeout = 10000, // 10 seconds timeout for each request
|
||||
RequestConfiguration = new RequestConfiguration
|
||||
{
|
||||
UserAgent = _defaultHeaders.GetValueOrDefault("User-Agent")
|
||||
}
|
||||
};
|
||||
|
||||
DownloadService downloader = new(downloadConfiguration);
|
||||
|
||||
downloader.DownloadProgressChanged += onProgressChanged;
|
||||
downloader.DownloadFileCompleted += onProgressCompleted;
|
||||
|
||||
try
|
||||
{
|
||||
await downloader.DownloadFileTaskAsync(url, Path.Combine(savePath, fileName));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Info($"Download failed: {ex}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
15
VideoDownload/Platforms/DownloadPlatforms.cs
Normal file
15
VideoDownload/Platforms/DownloadPlatforms.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.ComponentModel;
|
||||
using Downloader;
|
||||
|
||||
namespace ACBuildService;
|
||||
|
||||
public abstract class DownloadPlatforms
|
||||
{
|
||||
public abstract Task<string> ExtractUrlAsync(string text);
|
||||
public abstract Task<VideoModel> ParseShare(string DownloadUrlText);
|
||||
public abstract Task<VideoModel> ExtractVideoDataAsync(string url);
|
||||
|
||||
public abstract Task<bool> DownloadAsync(string url, string savePath, string fileName,
|
||||
EventHandler<DownloadProgressChangedEventArgs> onProgressChanged,
|
||||
EventHandler<AsyncCompletedEventArgs> onProgressCompleted);
|
||||
}
|
||||
53
VideoDownload/Platforms/KuaiShou.cs
Normal file
53
VideoDownload/Platforms/KuaiShou.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.ComponentModel;
|
||||
using Downloader;
|
||||
|
||||
namespace ACBuildService;
|
||||
|
||||
public class KuaiShou : DownloadPlatforms
|
||||
{
|
||||
private static readonly Dictionary<string, string> _defaultHeaders = new()
|
||||
{
|
||||
{
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36"
|
||||
},
|
||||
{ "sec-fetch-site", "same-origin" },
|
||||
{ "sec-fetch-mode", "cors" },
|
||||
{ "sec-fetch-dest", "empty" },
|
||||
{ "sec-ch-ua-platform", "Windows" },
|
||||
{ "sec-ch-ua-mobile", "?0" },
|
||||
{ "sec-ch-ua", "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\"" },
|
||||
{ "referer", "https://www.douyin.com/?recommend=1" },
|
||||
{ "priority", "u=1, i" },
|
||||
{ "pragma", "no-cache" },
|
||||
{ "cache-control", "no-cache" },
|
||||
{ "accept-language", "zh-CN,zh;q=0.9,en;q=0.8" },
|
||||
{
|
||||
"accept",
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
|
||||
},
|
||||
{ "dnt", "1" }
|
||||
};
|
||||
|
||||
public override Task<string> ExtractUrlAsync(string text)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<VideoModel> ParseShare(string DownloadUrlText)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<VideoModel> ExtractVideoDataAsync(string url)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<bool> DownloadAsync(string url, string savePath, string fileName,
|
||||
EventHandler<DownloadProgressChangedEventArgs> onProgressChanged,
|
||||
EventHandler<AsyncCompletedEventArgs> onProgressCompleted)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
151
VideoDownload/VideoDownload.cs
Normal file
151
VideoDownload/VideoDownload.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace ACBuildService;
|
||||
|
||||
public static class VideoDownload
|
||||
{
|
||||
private static readonly Channel<string> _queue = Channel.CreateUnbounded<string>();
|
||||
private static readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
||||
private static readonly TimeSpan _interval = TimeSpan.FromSeconds(20);
|
||||
|
||||
|
||||
static VideoDownload()
|
||||
{
|
||||
Task.Run(ProcessQueueAsync);
|
||||
}
|
||||
|
||||
public static void Add(string shareText)
|
||||
{
|
||||
_queue.Writer.TryWrite(shareText);
|
||||
}
|
||||
|
||||
private static async Task ProcessQueueAsync()
|
||||
{
|
||||
var timer = new PeriodicTimer(_interval);
|
||||
|
||||
try
|
||||
{
|
||||
while (await _queue.Reader.WaitToReadAsync(_cts.Token) &&
|
||||
await timer.WaitForNextTickAsync(_cts.Token))
|
||||
{
|
||||
if (_queue.Reader.TryRead(out var shareText))
|
||||
{
|
||||
// 这里开始处理任务,这个处理过程本身的时间不计入间隔
|
||||
// PeriodicTimer 会保证两次 WaitForNextTickAsync 之间间隔10秒
|
||||
await ProcessVideoDownloadAsync(shareText);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常取消
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ProcessVideoDownloadAsync(string shareText)
|
||||
{
|
||||
try
|
||||
{
|
||||
var service = GetService(shareText);
|
||||
if (service != null)
|
||||
{
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 开始处理: {shareText}");
|
||||
await DownloadVideoAsync(service, shareText);
|
||||
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 处理完成: {shareText}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 处理失败: {shareText}, 错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟下载视频的方法
|
||||
private static async Task DownloadVideoAsync(DownloadPlatforms service, string shareText)
|
||||
{
|
||||
//开始解析数据
|
||||
var videoModel = await service.ParseShare(shareText);
|
||||
if (videoModel == null) return;
|
||||
|
||||
var rootPath = $"{HttpService.BaseRootPath}videos/";
|
||||
var t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var fileName = $"{t}.mp4";
|
||||
var filePath = Path.Combine(rootPath, fileName);
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
Log.Error("文件已存在");
|
||||
return;
|
||||
}
|
||||
|
||||
var ret = await service.DownloadAsync(videoModel.VideoUrl, rootPath, fileName, null, null);
|
||||
if (ret)
|
||||
{
|
||||
var dbItem = new VideoTable
|
||||
{
|
||||
SourcePath = shareText,
|
||||
FilePath = fileName,
|
||||
AuthorName = videoModel.AuthorName,
|
||||
Title = videoModel.Title,
|
||||
Desc = videoModel.Desc,
|
||||
Duration = videoModel.Duration,
|
||||
Like = (int)videoModel.DiggCount,
|
||||
Collect = (int)videoModel.CollectCount,
|
||||
Message = (int)videoModel.CommentCount,
|
||||
Share = (int)videoModel.ShareCount,
|
||||
CreateTime = DateTime.Now
|
||||
};
|
||||
DB.Main.Insertable(dbItem).ExecuteCommand();
|
||||
}
|
||||
|
||||
//解析数据后等待一会
|
||||
await Task.Delay(3000);
|
||||
}
|
||||
|
||||
#region Service
|
||||
|
||||
private static Dictionary<ShortVideoPlatformEnum, DownloadPlatforms> _services =
|
||||
new Dictionary<ShortVideoPlatformEnum, DownloadPlatforms>();
|
||||
|
||||
private static DownloadPlatforms GetService(string text)
|
||||
{
|
||||
var platform = GetPlatform(text);
|
||||
if (_services.TryGetValue(platform, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (platform == ShortVideoPlatformEnum.DouYin)
|
||||
{
|
||||
value = new DouYin();
|
||||
}
|
||||
else if (platform == ShortVideoPlatformEnum.KuaiShou)
|
||||
{
|
||||
value = new KuaiShou();
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
_services[platform] = value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static ShortVideoPlatformEnum GetPlatform(string text)
|
||||
{
|
||||
if (text.Contains("douyin"))
|
||||
{
|
||||
return ShortVideoPlatformEnum.DouYin;
|
||||
}
|
||||
|
||||
if (text.Contains("kuaishou"))
|
||||
{
|
||||
return ShortVideoPlatformEnum.KuaiShou;
|
||||
}
|
||||
|
||||
return ShortVideoPlatformEnum.None;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
200
VideoDownload/VideoDownloadService.cs
Normal file
200
VideoDownload/VideoDownloadService.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
// using System.ComponentModel;
|
||||
// using System.Text.RegularExpressions;
|
||||
// using Downloader;
|
||||
// using Newtonsoft.Json;
|
||||
// using RestSharp;
|
||||
//
|
||||
// namespace ACBuildService;
|
||||
//
|
||||
// public class VideoDownloadService
|
||||
// {
|
||||
// private static readonly Dictionary<string, string> _defaultHeaders = new()
|
||||
// {
|
||||
// {
|
||||
// "User-Agent",
|
||||
// "Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36"
|
||||
// },
|
||||
// { "sec-fetch-site", "same-origin" },
|
||||
// { "sec-fetch-mode", "cors" },
|
||||
// { "sec-fetch-dest", "empty" },
|
||||
// { "sec-ch-ua-platform", "Windows" },
|
||||
// { "sec-ch-ua-mobile", "?0" },
|
||||
// { "sec-ch-ua", "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\"" },
|
||||
// { "referer", "https://www.douyin.com/?recommend=1" },
|
||||
// { "priority", "u=1, i" },
|
||||
// { "pragma", "no-cache" },
|
||||
// { "cache-control", "no-cache" },
|
||||
// { "accept-language", "zh-CN,zh;q=0.9,en;q=0.8" },
|
||||
// {
|
||||
// "accept",
|
||||
// "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
|
||||
// },
|
||||
// { "dnt", "1" }
|
||||
// };
|
||||
//
|
||||
// public async Task<VideoModel> ParseShare(string DownloadUrlText)
|
||||
// {
|
||||
// if (!DownloadUrlText.Contains("https://"))
|
||||
// {
|
||||
// // throw new ValidationException("请输入正确的分享链接");
|
||||
// Log.Error("请输入正确的分享链接");
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// VideoModel Data = null;
|
||||
//
|
||||
// if (DownloadUrlText.Contains(ShortVideoPlatformEnum.DouYin.ToString().ToLower()))
|
||||
// {
|
||||
// var downloadUrl = await ExtractUrlAsync(DownloadUrlText);
|
||||
// Data = await ExtractVideoDataAsync(downloadUrl);
|
||||
// Data.ShareId = downloadUrl.Replace("https://v.douyin.com/", "").Replace("/", "");
|
||||
// }
|
||||
//
|
||||
// // else if (DownloadUrlText.Contains(ShortVideoPlatformEnum.KuaiShou.ToString().ToLower()))
|
||||
// // {
|
||||
// // var downloadUrl = await _kuaiShortVideoService.ExtractUrlAsync(DownloadUrlText);
|
||||
// // Data = await _kuaiShortVideoService.ExtractVideoDataAsync(downloadUrl);
|
||||
// // }
|
||||
// // else
|
||||
// // {
|
||||
// // throw new ValidationException("暂不支持该平台");
|
||||
// // }
|
||||
// // Log.Info($"解析结果 data={JSON.Serialize(Data)}");
|
||||
// return Data;
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 解析dy分享中的文本 6.17 05/18 U@L.wf fbn:/ 悬疑推理:亡者和自己的手机上午一同下葬,下午却给警察发来短信 本期的故事,来自于高分推理神剧《天堂岛疑云》中的谜案《亡灵的短信》。# 悬疑推理 # 每日推荐电影
|
||||
// /// # 一剪到底 https://v.douyin.com/ircqoExo/ 复制此链接,打开Dou音搜索,直接观看视频!
|
||||
// /// https://v.douyin.com/ircqoExo/
|
||||
// /// </summary>
|
||||
// /// <param name="text"></param>
|
||||
// /// <returns></returns>
|
||||
// public async Task<string> ExtractUrlAsync(string text)
|
||||
// {
|
||||
// Log.Info($"开始解析抖音链接 {text}");
|
||||
// return Regex.Match(text, @"https?://[^\s]+").Value;
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 根据链接 https://v.douyin.com/ircqoExo/ 解析出页面中的数据
|
||||
// /// </summary>
|
||||
// /// <param name="url"></param>
|
||||
// /// <returns></returns>
|
||||
// public Task<VideoModel> ExtractVideoDataAsync(string url)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// Log.Info($"开始解析链接 {url}");
|
||||
// // 创建RestClient
|
||||
// RestClient client = new(url);
|
||||
//
|
||||
// // 创建请求对象
|
||||
// RestRequest request = new();
|
||||
//
|
||||
// // 设置 User-Agent 模拟手机浏览器
|
||||
// request.AddHeaders(_defaultHeaders);
|
||||
//
|
||||
// // 发送请求并获取响应
|
||||
// var response = client.Execute(request);
|
||||
// if (!response.IsSuccessful) throw new HttpRequestException("request is fail");
|
||||
//
|
||||
// var content = response.Content;
|
||||
// Log.Info($"开始解析响应内容 {content}");
|
||||
// if (content is null) throw new InvalidDataException("content is null");
|
||||
//
|
||||
// const string routerDataPattern = @"_ROUTER_DATA\s*=\s*(\{.*?\})<";
|
||||
//
|
||||
// var matchJson = Regex.Match(content, routerDataPattern);
|
||||
//
|
||||
// Log.Info($"开始解析匹配到的json {matchJson}");
|
||||
// if (matchJson.Groups.Count < 2) throw new InvalidDataException("未匹配到合法的数据,matchJson.Groups.Count < 2");
|
||||
//
|
||||
// var videoJson = matchJson.Groups[1].Value;
|
||||
// Log.Info($"开始解析匹配到的json {videoJson}");
|
||||
// // 反序列化JSON字符串为C#对象
|
||||
// var videoData = JsonConvert.DeserializeObject<DouYinShareRouterData>(videoJson);
|
||||
//
|
||||
// if (videoData is null) throw new InvalidDataException("JSON解析数据为空,请检查分享链接是否正确,如有更多问题请查看日志");
|
||||
//
|
||||
// var videoInfoData = videoData.LoaderData.VideoIdPage.VideoInfoRes.ItemList.First();
|
||||
//
|
||||
// var video = new VideoModel
|
||||
// {
|
||||
// Platform = ShortVideoPlatformEnum.DouYin,
|
||||
// VideoId = videoInfoData.AwemeId,
|
||||
// AuthorName = videoInfoData.Author.Nickname,
|
||||
// UniqueId = videoInfoData.Author.UniqueId == ""
|
||||
// ? videoInfoData.Author.ShortId
|
||||
// : videoInfoData.Author.UniqueId,
|
||||
// AuthorAvatar = videoInfoData.Author.AvatarThumb.UrlList.First().ToString(),
|
||||
// Title = videoInfoData.Author.Signature,
|
||||
// Cover = videoInfoData.Video.Cover.UrlList.Last().ToString(),
|
||||
// Mp3Url = "",
|
||||
// CreatedTime =
|
||||
// DateTimeOffset.FromUnixTimeSeconds(videoInfoData.CreateTime)
|
||||
// .ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
// Desc = videoInfoData.Desc,
|
||||
// Duration = "",
|
||||
// DiggCount = videoInfoData.Statistics.DiggCount,
|
||||
// CollectCount = videoInfoData.Statistics.CollectCount,
|
||||
// CommentCount = videoInfoData.Statistics.CommentCount,
|
||||
// ShareCount = videoInfoData.Statistics.ShareCount
|
||||
// };
|
||||
// switch (videoInfoData.AwemeType)
|
||||
// {
|
||||
// case 2:
|
||||
// video.VideoUrl = videoInfoData.Video.Cover.UrlList.First().ToString();
|
||||
// break;
|
||||
// default:
|
||||
// video.VideoUrl = videoInfoData.Video.PlayAddr.UrlList.First().ToString().Replace("playwm", "play");
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// if (video.VideoUrl.Contains("ratio=720p"))
|
||||
// {
|
||||
// video.VideoUrl = video.VideoUrl.Replace("ratio=720p", "ratio=1080p");
|
||||
// }
|
||||
//
|
||||
// return Task.FromResult(video);
|
||||
// }
|
||||
// catch (Exception e)
|
||||
// {
|
||||
// Log.Error(e.Message);
|
||||
// throw;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// public async Task DownloadAsync(string url, string savePath, string fileName,
|
||||
// EventHandler<DownloadProgressChangedEventArgs> onProgressChanged,
|
||||
// EventHandler<AsyncCompletedEventArgs> onProgressCompleted
|
||||
// )
|
||||
// {
|
||||
// DownloadConfiguration downloadConfiguration = new()
|
||||
// {
|
||||
// ChunkCount = 8, // Download in 8 chunks (increase for larger files)
|
||||
// MaxTryAgainOnFailure = 5,
|
||||
// Timeout = 10000, // 10 seconds timeout for each request
|
||||
// RequestConfiguration = new RequestConfiguration
|
||||
// {
|
||||
// UserAgent = _defaultHeaders.GetValueOrDefault("User-Agent")
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// DownloadService downloader = new(downloadConfiguration);
|
||||
//
|
||||
// downloader.DownloadProgressChanged += onProgressChanged;
|
||||
// downloader.DownloadFileCompleted += onProgressCompleted;
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// await downloader.DownloadFileTaskAsync(url, savePath + fileName);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Info($"Download failed: {ex}");
|
||||
// throw;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
70
VideoDownload/VideoModel.cs
Normal file
70
VideoDownload/VideoModel.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
namespace ACBuildService;
|
||||
|
||||
public enum ShortVideoPlatformEnum
|
||||
{
|
||||
None,
|
||||
// 抖音
|
||||
DouYin,
|
||||
|
||||
// 快手
|
||||
KuaiShou,
|
||||
|
||||
/// <summary>
|
||||
/// B站
|
||||
/// </summary>
|
||||
Bilibili,
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class VideoModel
|
||||
{
|
||||
// 短视频所属的平台
|
||||
public ShortVideoPlatformEnum Platform { get; set; }
|
||||
|
||||
// 视频ID
|
||||
public string VideoId { get; set; }
|
||||
|
||||
// 作者
|
||||
public string AuthorName { get; set; }
|
||||
|
||||
// Unique Id
|
||||
public string UniqueId { get; set; }
|
||||
|
||||
// 头像
|
||||
public string AuthorAvatar { get; set; }
|
||||
|
||||
// 视频标题
|
||||
public string Title { get; set; }
|
||||
|
||||
// 封面
|
||||
public string Cover { get; set; }
|
||||
|
||||
// 收藏数
|
||||
public long CollectCount { get; set; }
|
||||
|
||||
// 点赞数
|
||||
public long DiggCount { get; set; }
|
||||
|
||||
// 分享数
|
||||
public long ShareCount { get; set; }
|
||||
|
||||
// 评论数
|
||||
public long CommentCount { get; set; }
|
||||
|
||||
// 视频地址
|
||||
public string VideoUrl { get; set; }
|
||||
|
||||
// 音频地址
|
||||
public string Mp3Url { get; set; }
|
||||
|
||||
// 创建时间
|
||||
public string CreatedTime { get; set; }
|
||||
|
||||
// 视频描述
|
||||
public string Desc { get; set; }
|
||||
|
||||
// 视频时长
|
||||
public string Duration { get; set; }
|
||||
|
||||
public string ShareId { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user