diff --git a/src/BlueCatKoKo.Ui/App.xaml.cs b/src/BlueCatKoKo.Ui/App.xaml.cs index 2b7061f..d5742d6 100644 --- a/src/BlueCatKoKo.Ui/App.xaml.cs +++ b/src/BlueCatKoKo.Ui/App.xaml.cs @@ -51,7 +51,8 @@ public partial class App : Application // Service containing container.AddSingleton(); - container.AddSingleton(); + container.AddTransient(); + container.AddTransient(); // Main window with navigation container.AddSingleton(); diff --git a/src/BlueCatKoKo.Ui/Constants/ShortVideoPlatformEnum.cs b/src/BlueCatKoKo.Ui/Constants/ShortVideoPlatformEnum.cs new file mode 100644 index 0000000..0bca8fc --- /dev/null +++ b/src/BlueCatKoKo.Ui/Constants/ShortVideoPlatformEnum.cs @@ -0,0 +1,10 @@ +namespace BlueCatKoKo.Ui.Constants +{ + public enum ShortVideoPlatformEnum + { + // 抖音 + DouYin, + // 快手 + KuaiShou, + } +} \ No newline at end of file diff --git a/src/BlueCatKoKo.Ui/Models/DouyinShareRouterData.cs b/src/BlueCatKoKo.Ui/Models/DouyinShareRouterData.cs index d4af007..09e7a51 100644 --- a/src/BlueCatKoKo.Ui/Models/DouyinShareRouterData.cs +++ b/src/BlueCatKoKo.Ui/Models/DouyinShareRouterData.cs @@ -5,7 +5,7 @@ namespace BlueCatKoKo.Ui.Models /// /// 抖音 分享文本中的视频数据 /// - public class DouyinShareRouterData + public class DouYinShareRouterData { [JsonProperty("loaderData")] public LoaderData LoaderData { get; set; } } @@ -258,17 +258,17 @@ public class RiskInfos public class Statistics { - [JsonProperty("comment_count")] public int CommentCount { get; set; } + [JsonProperty("comment_count")] public long CommentCount { get; set; } - [JsonProperty("share_count")] public int ShareCount { get; set; } + [JsonProperty("share_count")] public long ShareCount { get; set; } [JsonProperty("aweme_id")] public string AwemeId { get; set; } - [JsonProperty("digg_count")] public int DiggCount { get; set; } + [JsonProperty("digg_count")] public long DiggCount { get; set; } - [JsonProperty("play_count")] public int PlayCount { get; set; } + [JsonProperty("play_count")] public long PlayCount { get; set; } - [JsonProperty("collect_count")] public int CollectCount { get; set; } + [JsonProperty("collect_count")] public long CollectCount { get; set; } } public class TextExtra diff --git a/src/BlueCatKoKo.Ui/Models/KuaiShouShareVideoData.cs b/src/BlueCatKoKo.Ui/Models/KuaiShouShareVideoData.cs new file mode 100644 index 0000000..b3b243e --- /dev/null +++ b/src/BlueCatKoKo.Ui/Models/KuaiShouShareVideoData.cs @@ -0,0 +1,631 @@ +// YApi QuickType插件生成,具体参考文档:https://plugins.jetbrains.com/plugin/18847-yapi-quicktype/documentation + +using Newtonsoft.Json; + +namespace BlueCatKoKo.Ui.Models +{ + public class KuaiShouShareVideoData + { + [JsonProperty("result")] + public long Result { get; set; } + + [JsonProperty("fid")] + public long Fid { get; set; } + + [JsonProperty("shareInfo")] + public ShareInfo ShareInfo { get; set; } + + [JsonProperty("comments")] + public List Comments { get; set; } + + [JsonProperty("counts")] + public Counts Counts { get; set; } + + [JsonProperty("photo")] + public Photo Photo { get; set; } + + [JsonProperty("trendingInfo")] + public TrendingInfo TrendingInfo { get; set; } + + [JsonProperty("mp4Url")] + public Uri Mp4Url { get; set; } + + [JsonProperty("photos")] + public List Photos { get; set; } + + [JsonProperty("shareUserPhotos")] + public List ShareUserPhotos { get; set; } + + [JsonProperty("serialInfo")] + public SerialInfo SerialInfo { get; set; } + } + + public class Comment + { + [JsonProperty("author_name")] + public string AuthorName { get; set; } + + [JsonProperty("cashTags")] + public CashTags CashTags { get; set; } + + [JsonProperty("commentAuthorTags")] + public List CommentAuthorTags { get; set; } + + [JsonProperty("photo_id")] + public double PhotoId { get; set; } + + [JsonProperty("headurl")] + public Uri Headurl { get; set; } + + [JsonProperty("headurls")] + public List Headurls { get; set; } + + [JsonProperty("author_liked")] + public bool AuthorLiked { get; set; } + + [JsonProperty("type")] + public long Type { get; set; } + + [JsonProperty("comment_id")] + public long CommentId { get; set; } + + [JsonProperty("authorVerified")] + public bool AuthorVerified { get; set; } + + [JsonProperty("content")] + public string Content { get; set; } + + [JsonProperty("user_sex")] + public string UserSex { get; set; } + + [JsonProperty("reply_to")] + public long ReplyTo { get; set; } + + [JsonProperty("user_id")] + public long UserId { get; set; } + + [JsonProperty("replyToUserName", NullValueHandling = NullValueHandling.Ignore)] + public string ReplyToUserName { get; set; } + + [JsonProperty("commentBottomTags")] + public List CommentBottomTags { get; set; } + + [JsonProperty("time")] + public DateTimeOffset Time { get; set; } + + [JsonProperty("author_id")] + public long AuthorId { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } + + public class CashTags + { + } + + public class CoverUrl + { + [JsonProperty("cdn")] + public string Cdn { get; set; } + + [JsonProperty("url")] + public Uri Url { get; set; } + } + + public class Counts + { + [JsonProperty("fanCount")] + public long FanCount { get; set; } + + [JsonProperty("followCount")] + public long FollowCount { get; set; } + + [JsonProperty("collectionCount")] + public long CollectionCount { get; set; } + + [JsonProperty("photoCount")] + public long PhotoCount { get; set; } + } + + public class Photo + { + [JsonProperty("adminTags")] + public List AdminTags { get; set; } + + [JsonProperty("soundTrack")] + public SoundTrack SoundTrack { get; set; } + + [JsonProperty("headUrl")] + public Uri HeadUrl { get; set; } + + [JsonProperty("caption")] + public string Caption { get; set; } + + [JsonProperty("forcePublic")] + public string ForcePublic { get; set; } + + [JsonProperty("likeCount")] + public long LikeCount { get; set; } + + [JsonProperty("exp_tag")] + public string ExpTag { get; set; } + + [JsonProperty("type")] + public long Type { get; set; } + + [JsonProperty("mainMvUrls")] + public List MainMvUrls { get; set; } + + [JsonProperty("userEid")] + public string UserEid { get; set; } + + [JsonProperty("duration")] + public long Duration { get; set; } + + [JsonProperty("shareCount", NullValueHandling = NullValueHandling.Ignore)] + public long? ShareCount { get; set; } + + [JsonProperty("serverExpTag")] + public string ServerExpTag { get; set; } + + [JsonProperty("ext_params")] + public ExtParams ExtParams { get; set; } + + [JsonProperty("viewCount")] + public long ViewCount { get; set; } + + [JsonProperty("headUrls")] + public List HeadUrls { get; set; } + + [JsonProperty("forwardCount")] + public long ForwardCount { get; set; } + + [JsonProperty("tagShow")] + public TagShow TagShow { get; set; } + + [JsonProperty("singlePicture")] + public bool SinglePicture { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + + [JsonProperty("height")] + public long Height { get; set; } + + [JsonProperty("webpCoverUrls")] + public List WebpCoverUrls { get; set; } + + [JsonProperty("userSex")] + public string UserSex { get; set; } + + [JsonProperty("manifest", NullValueHandling = NullValueHandling.Ignore)] + public Manifest Manifest { get; set; } + + [JsonProperty("share_info")] + public string ShareInfo { get; set; } + + [JsonProperty("sameFrame")] + public SameFrame SameFrame { get; set; } + + [JsonProperty("verified")] + public bool Verified { get; set; } + + [JsonProperty("photoId")] + public string PhotoId { get; set; } + + [JsonProperty("userName")] + public string UserName { get; set; } + + [JsonProperty("userId")] + public long UserId { get; set; } + + [JsonProperty("commentCount")] + public long CommentCount { get; set; } + + [JsonProperty("commentShowType", NullValueHandling = NullValueHandling.Ignore)] + public long? CommentShowType { get; set; } + + [JsonProperty("width")] + public long Width { get; set; } + + [JsonProperty("photoType")] + public string PhotoType { get; set; } + + [JsonProperty("coverUrls")] + public List CoverUrls { get; set; } + + [JsonProperty("cccCoverMap", NullValueHandling = NullValueHandling.Ignore)] + public CccCoverMap CccCoverMap { get; set; } + + [JsonProperty("overrideCoverUrls", NullValueHandling = NullValueHandling.Ignore)] + public OverrideCoverUrls OverrideCoverUrls { get; set; } + } + + public class CccCoverMap + { + [JsonProperty("width")] + public long Width { get; set; } + + [JsonProperty("height")] + public long Height { get; set; } + } + + public class ExtParams + { + [JsonProperty("mtype")] + public long Mtype { get; set; } + + [JsonProperty("color")] + public string Color { get; set; } + + [JsonProperty("w")] + public long W { get; set; } + + [JsonProperty("sound")] + public long Sound { get; set; } + + [JsonProperty("h")] + public long H { get; set; } + + [JsonProperty("interval")] + public long Interval { get; set; } + + [JsonProperty("video")] + public long Video { get; set; } + } + + public class Manifest + { + [JsonProperty("playInfo")] + public PlayInfo PlayInfo { get; set; } + + [JsonProperty("manualDefaultSelect")] + public bool ManualDefaultSelect { get; set; } + + [JsonProperty("videoFeature")] + public VideoFeature VideoFeature { get; set; } + + [JsonProperty("stereoType")] + public long StereoType { get; set; } + + [JsonProperty("adaptationSet")] + public List AdaptationSet { get; set; } + + [JsonProperty("mediaType")] + public long MediaType { get; set; } + + [JsonProperty("videoId")] + public string VideoId { get; set; } + + [JsonProperty("hideAuto")] + public bool HideAuto { get; set; } + + [JsonProperty("businessType")] + public long BusinessType { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } + } + + public class AdaptationSet + { + [JsonProperty("duration")] + public long Duration { get; set; } + + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("representation")] + public List Representation { get; set; } + } + + public class Representation + { + [JsonProperty("avgBitrate")] + public long AvgBitrate { get; set; } + + [JsonProperty("hidden")] + public bool Hidden { get; set; } + + [JsonProperty("backupUrl")] + public List BackupUrl { get; set; } + + [JsonProperty("maxBitrate")] + public long MaxBitrate { get; set; } + + [JsonProperty("defaultSelect")] + public bool DefaultSelect { get; set; } + + [JsonProperty("hdrType")] + public long HdrType { get; set; } + + [JsonProperty("url")] + public Uri Url { get; set; } + + [JsonProperty("p2spCode")] + public string P2SpCode { get; set; } + + [JsonProperty("quality")] + public double Quality { get; set; } + + [JsonProperty("featureP2sp")] + public bool FeatureP2Sp { get; set; } + + [JsonProperty("frameRate")] + public double FrameRate { get; set; } + + [JsonProperty("qualityLabel")] + public string QualityLabel { get; set; } + + [JsonProperty("fileSize")] + public long FileSize { get; set; } + + [JsonProperty("width")] + public long Width { get; set; } + + [JsonProperty("comment")] + public string Comment { get; set; } + + [JsonProperty("bitratePattern")] + public List BitratePattern { get; set; } + + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("qualityType")] + public string QualityType { get; set; } + + [JsonProperty("kvqScore")] + public KvqScore KvqScore { get; set; } + + [JsonProperty("disableAdaptive")] + public bool DisableAdaptive { get; set; } + + [JsonProperty("minorInfo")] + public string MinorInfo { get; set; } + + [JsonProperty("videoCodec")] + public string VideoCodec { get; set; } + + [JsonProperty("height")] + public long Height { get; set; } + } + + public class KvqScore + { + [JsonProperty("FRPost")] + public long FrPost { get; set; } + + [JsonProperty("NR")] + public double Nr { get; set; } + + [JsonProperty("NRPost")] + public double NrPost { get; set; } + + [JsonProperty("FR")] + public double Fr { get; set; } + } + + public class PlayInfo + { + [JsonProperty("strategyBus")] + public string StrategyBus { get; set; } + + [JsonProperty("cdnTimeRangeLevel")] + public long CdnTimeRangeLevel { get; set; } + } + + public class VideoFeature + { + [JsonProperty("mosScore")] + public double MosScore { get; set; } + + [JsonProperty("blurProbability")] + public double BlurProbability { get; set; } + + [JsonProperty("blockyProbability")] + public double BlockyProbability { get; set; } + + [JsonProperty("avgEntropy")] + public double AvgEntropy { get; set; } + } + + public class OverrideCoverUrls + { + [JsonProperty("jpgCdnNodeView")] + public List JpgCdnNodeView { get; set; } + + [JsonProperty("webpCdnNodeView")] + public List WebpCdnNodeView { get; set; } + } + + public class SameFrame + { + [JsonProperty("allow")] + public bool Allow { get; set; } + + [JsonProperty("availableDepth")] + public long AvailableDepth { get; set; } + } + + public class SoundTrack + { + [JsonProperty("genreId")] + public long GenreId { get; set; } + + [JsonProperty("audioUrls")] + public List AudioUrls { get; set; } + + [JsonProperty("finalStatus")] + public long FinalStatus { get; set; } + + [JsonProperty("avatarUrls")] + public List AvatarUrls { get; set; } + + [JsonProperty("artist")] + public string Artist { get; set; } + + [JsonProperty("hasCopyright")] + public bool HasCopyright { get; set; } + + [JsonProperty("loudness")] + public long Loudness { get; set; } + + [JsonProperty("audioType")] + public long AudioType { get; set; } + + [JsonProperty("photoId")] + public double PhotoId { get; set; } + + [JsonProperty("type")] + public long Type { get; set; } + + [JsonProperty("disableEnhancedEntry")] + public bool DisableEnhancedEntry { get; set; } + + [JsonProperty("imageUrls")] + public List ImageUrls { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("isOffline")] + public bool IsOffline { get; set; } + + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("user")] + public User User { get; set; } + } + + public class User + { + [JsonProperty("user_sex")] + public string UserSex { get; set; } + + [JsonProperty("eid")] + public string Eid { get; set; } + + [JsonProperty("user_id")] + public long UserId { get; set; } + + [JsonProperty("user_name")] + public string UserName { get; set; } + + [JsonProperty("following")] + public bool Following { get; set; } + + [JsonProperty("headurl")] + public Uri Headurl { get; set; } + + [JsonProperty("headurls")] + public List Headurls { get; set; } + + [JsonProperty("visitorBeFollowed")] + public bool VisitorBeFollowed { get; set; } + } + + public class TagShow + { + [JsonProperty("bannerType")] + public long BannerType { get; set; } + + [JsonProperty("bizId")] + public string BizId { get; set; } + + [JsonProperty("type")] + public long Type { get; set; } + + [JsonProperty("usedCount")] + public long UsedCount { get; set; } + } + + public class SerialInfo + { + [JsonProperty("valid")] + public bool Valid { get; set; } + + [JsonProperty("show")] + public bool Show { get; set; } + } + + public class ShareInfo + { + [JsonProperty("groupName")] + public string GroupName { get; set; } + + [JsonProperty("shareTitle")] + public string ShareTitle { get; set; } + + [JsonProperty("docId")] + public long DocId { get; set; } + + [JsonProperty("shareId")] + public string ShareId { get; set; } + + [JsonProperty("shareType")] + public long ShareType { get; set; } + + [JsonProperty("shareSubTitle")] + public string ShareSubTitle { get; set; } + } + + public class TrendingInfo + { + [JsonProperty("iconRightMargin")] + public long IconRightMargin { get; set; } + + [JsonProperty("preTitle")] + public string PreTitle { get; set; } + + [JsonProperty("backgroundColor")] + public string BackgroundColor { get; set; } + + [JsonProperty("iconHeight")] + public long IconHeight { get; set; } + + [JsonProperty("visibility")] + public long Visibility { get; set; } + + [JsonProperty("iconWidth")] + public long IconWidth { get; set; } + + [JsonProperty("showSeparator")] + public bool ShowSeparator { get; set; } + + [JsonProperty("wordId")] + public long WordId { get; set; } + + [JsonProperty("notTruncatedTitle")] + public string NotTruncatedTitle { get; set; } + + [JsonProperty("bottomWeakStyleType")] + public long BottomWeakStyleType { get; set; } + + [JsonProperty("showArrow")] + public bool ShowArrow { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("tailIconUrl")] + public string TailIconUrl { get; set; } + + [JsonProperty("roundCorner")] + public bool RoundCorner { get; set; } + + [JsonProperty("enableForceClose")] + public bool EnableForceClose { get; set; } + + [JsonProperty("fontSize")] + public long FontSize { get; set; } + + [JsonProperty("iconUrl")] + public Uri IconUrl { get; set; } + + [JsonProperty("fontColor")] + public string FontColor { get; set; } + } +} diff --git a/src/BlueCatKoKo.Ui/Models/VideoModel.cs b/src/BlueCatKoKo.Ui/Models/VideoModel.cs index 57226f3..e52ded5 100644 --- a/src/BlueCatKoKo.Ui/Models/VideoModel.cs +++ b/src/BlueCatKoKo.Ui/Models/VideoModel.cs @@ -1,10 +1,15 @@ -namespace BlueCatKoKo.Ui.Models.Pages +using BlueCatKoKo.Ui.Constants; + +namespace BlueCatKoKo.Ui.Models { /// /// home page页面数据 /// public class VideoModel { + // 短视频所属的平台 + public ShortVideoPlatformEnum Platform { get; set; } + // 视频ID public string? VideoId { get; set; } @@ -21,16 +26,19 @@ public class VideoModel public string? Cover { get; set; } // 收藏数 - public int? CollectCount { get; set; } + public long? CollectCount { get; set; } // 点赞数 - public int? DiggCount { get; set; } + public long? DiggCount { get; set; } // 分享数 - public int? ShareCount { get; set; } + public long? ShareCount { get; set; } // 评论数 - public int? CommentCount { get; set; } + public long? CommentCount { get; set; } + + // 观看数量 + public long? ViewCount { get; set; } // 视频地址 public string? VideoUrl { get; set; } diff --git a/src/BlueCatKoKo.Ui/Services/DouyinDownloaderService.cs b/src/BlueCatKoKo.Ui/Services/DouYinShortVideoService.cs similarity index 93% rename from src/BlueCatKoKo.Ui/Services/DouyinDownloaderService.cs rename to src/BlueCatKoKo.Ui/Services/DouYinShortVideoService.cs index 121862f..60d0093 100644 --- a/src/BlueCatKoKo.Ui/Services/DouyinDownloaderService.cs +++ b/src/BlueCatKoKo.Ui/Services/DouYinShortVideoService.cs @@ -3,8 +3,8 @@ using System.Net.Http; using System.Text.RegularExpressions; +using BlueCatKoKo.Ui.Constants; using BlueCatKoKo.Ui.Models; -using BlueCatKoKo.Ui.Models.Pages; using Downloader; @@ -16,7 +16,10 @@ namespace BlueCatKoKo.Ui.Services { - public class DouyinDownloaderService : IDownloaderService + /// + /// 抖音下载服务 + /// + public class DouYinShortVideoService : IShortVideoService { private static readonly Dictionary _defaultHeaders = new() { @@ -44,7 +47,7 @@ public class DouyinDownloaderService : IDownloaderService private readonly ILogger _logger; - public DouyinDownloaderService(ILogger logger) + public DouYinShortVideoService(ILogger logger) { _logger = logger; } @@ -58,7 +61,7 @@ public DouyinDownloaderService(ILogger logger) /// public async Task ExtractUrlAsync(string text) { - _logger.Information("开始解析文本 {text}", text); + _logger.Information("开始解析抖音链接 {text}", text); return Regex.Match(text, @"https?://[^\s]+").Value; } @@ -108,7 +111,7 @@ public async Task ExtractVideoDataAsync(string url) string videoJson = matchJson.Groups[1].Value; _logger.Information("开始解析匹配到的json {videoJson}", videoJson); // 反序列化JSON字符串为C#对象 - DouyinShareRouterData? videoData = JsonConvert.DeserializeObject(videoJson); + DouYinShareRouterData? videoData = JsonConvert.DeserializeObject(videoJson); if (videoData is null) { @@ -119,6 +122,7 @@ public async Task ExtractVideoDataAsync(string url) return new VideoModel { + Platform = ShortVideoPlatformEnum.DouYin, VideoId = videoInfoData.AwemeId, AuthorName = videoInfoData.Author.Nickname, AuthorAvatar = videoInfoData.Author.AvatarThumb.UrlList.First().ToString(), diff --git a/src/BlueCatKoKo.Ui/Services/IDownloaderService.cs b/src/BlueCatKoKo.Ui/Services/IDownloaderService.cs deleted file mode 100644 index f130883..0000000 --- a/src/BlueCatKoKo.Ui/Services/IDownloaderService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.ComponentModel; - -using BlueCatKoKo.Ui.Models.Pages; - -using Downloader; - -namespace BlueCatKoKo.Ui.Services -{ - public interface IDownloaderService - { - public Task ExtractUrlAsync(string text); - - public Task ExtractVideoDataAsync(string url); - - public Task DownloadAsync(string url, string savePath, string fileName, - EventHandler onProgressChanged, - EventHandler onProgressCompleted); - } -} \ No newline at end of file diff --git a/src/BlueCatKoKo.Ui/Services/IShortVideoService.cs b/src/BlueCatKoKo.Ui/Services/IShortVideoService.cs new file mode 100644 index 0000000..cbdcd4d --- /dev/null +++ b/src/BlueCatKoKo.Ui/Services/IShortVideoService.cs @@ -0,0 +1,19 @@ +using System.ComponentModel; + +using BlueCatKoKo.Ui.Models; + +using Downloader; + +namespace BlueCatKoKo.Ui.Services +{ + public interface IShortVideoService + { + Task ExtractUrlAsync(string text); + + Task ExtractVideoDataAsync(string url); + + Task DownloadAsync(string url, string savePath, string fileName, + EventHandler onProgressChanged, + EventHandler onProgressCompleted); + } +} \ No newline at end of file diff --git a/src/BlueCatKoKo.Ui/Services/KuaiShouShortVideoService.cs b/src/BlueCatKoKo.Ui/Services/KuaiShouShortVideoService.cs new file mode 100644 index 0000000..fc9b492 --- /dev/null +++ b/src/BlueCatKoKo.Ui/Services/KuaiShouShortVideoService.cs @@ -0,0 +1,251 @@ +using System.ComponentModel; +using System.IO; +using System.Text; +using System.Web; + +using BlueCatKoKo.Ui.Constants; +using BlueCatKoKo.Ui.Models; + +using Downloader; + +using Newtonsoft.Json; + +using RestSharp; + +using Serilog; + +namespace BlueCatKoKo.Ui.Services +{ + /// + /// 快手下载服务 + /// + public class KuaiShouShortVideoService:IShortVideoService + { + private readonly ILogger _logger; + + private static readonly Dictionary _locationHeaders = new() + { + { + "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" + }, + { "Accept-Language", "zh-CN,zh;q=0.9" }, + { "Cache-Control", "no-cache" }, + { "Connection", "keep-alive" }, + { "DNT", "1" }, + { "Host", "www.kuaishou.com" }, + { "Pragma", "no-cache" }, + { "Sec-Fetch-Dest", "document" }, + { "Sec-Fetch-Mode", "navigate" }, + { "Sec-Fetch-Site", "none" }, + { "Sec-Fetch-User", "?1" }, + { "Upgrade-Insecure-Requests", "1" }, + { + "UserAgent", + "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-ch-ua", "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"" }, + { "sec-ch-ua-mobile", "?1" }, + { "sec-ch-ua-platform", "\"Android\"" }, + { + "Cookie", + "kpf=PC_WEB; kpn=KUAISHOU_VISION; clientid=3; did=web_6ece7cfdd334f69ac1fe2579040329d0; didv=1725957114469" + } + }; + + private static Dictionary _videoInfoHeaders = new() + { + { "Accept", "*/*" }, + { "Accept-Encoding", "gzip, deflate, br, zstd" }, + { "Accept-Language", "zh-CN,zh;q=0.9" }, + { "Cache-Control", "no-cache" }, + { "Connection", "keep-alive" }, + { "Cookie", "did=web_d38fe86913df4a88b5593f8aa8d2638e; didv=1725940475000" }, + { "DNT", "1" }, + { "Host", "m.gifshow.com" }, + { "Origin", "https://m.gifshow.com" }, + { "Pragma", "no-cache" }, + // { + // "Referer", + // "https://m.gifshow.com/fw/photo/3xvfmfagspjxq9q?cc=share_copylink&kpf=PC_WEB&utm_campaign=pc_share&shareMethod=token&utm_medium=pc_share&kpn=KUAISHOU_VISION&subBiz=SINGLE_ROW_WEB&ztDid=web_126778f97e238efa29915c708f0789b6&shareId=18063407013272&shareToken=X-1KuDdzw7LGTYAM&shareMode=app&efid=0&shareObjectId=3xvfmfagspjxq9q&utm_source=pc_share" + // }, + { "Sec-Fetch-Dest", "empty" }, + { "Sec-Fetch-Mode", "cors" }, + { "Sec-Fetch-Site", "same-origin" }, + { + "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" + }, + { "content-type", "application/json" }, + { "sec-ch-ua", "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"" }, + { "sec-ch-ua-mobile", "?1" }, + { "sec-ch-ua-platform", "\"Android\"" } + }; + + // https://m.gifshow.com/rest/wd/photo/info 的body数据 + private static readonly Dictionary _requestBodyDict = new() + { + { "efid", "0" }, + { "shareMethod", "token" }, + { "shareChannel", "share_copylink" }, + { "kpn", "KUAISHOU_VISION" }, + { "subBiz", "SINGLE_ROW_WEB" }, + { "env", "SHARE_VIEWER_ENV_TX_TRICK" }, + { "h5Domain", "m.gifshow.com" }, + { "isLongVideo", "false" }, + // 下面的数据需要获取location中的数据 + { "shareToken", "X99ctjxn909o1jf" }, + { "shareObjectId", "3xvfmfagspjxq9q" }, + { "shareId", "18063552635341" }, + // 与shareObjectId数据一样 + { "photoId", "3xvfmfagspjxq9q" }, + }; + + + public KuaiShouShortVideoService(ILogger logger) + { + _logger = logger; + } + + /// + /// 解析快手 + /// https://www.kuaishou.com/f/X-9UuzlPQN2St1Ab + /// + /// + /// + public async Task ExtractUrlAsync(string text) + { + _logger.Information("开始解析快手链接 {text}", text); + return text.Trim(); + } + + /// + /// 根据链接 https://www.kuaishou.com/f/X-9UuzlPQN2St1Ab 解析出页面中的数据 + /// + /// + /// + public async Task ExtractVideoDataAsync(string url) + { + try + { + _logger.Information("开始解析链接 {url}", url); + + var client = new RestClient(); + var locationRequest = new RestRequest(url); + + locationRequest.AddHeaders(_locationHeaders); + var locationResponse = client.Execute(locationRequest); + + if (locationResponse.ResponseUri is null) + { + _logger.Error("获取location参数失败"); + throw new InvalidDataException("获取location参数失败"); + } + + var queryParams = HttpUtility.ParseQueryString(locationResponse.ResponseUri.Query); + + if (queryParams is null) + { + _logger.Error("解析链接失败"); + throw new InvalidDataException("获取response uri中的query params失败"); + } + + + _requestBodyDict["shareObjectId"] = queryParams.Get("shareObjectId"); + _requestBodyDict["shareId"] = queryParams.Get("shareId"); + _requestBodyDict["shareToken"] = queryParams.Get("shareToken"); + _requestBodyDict["photoId"] = queryParams.Get("shareObjectId"); + var videoInfoRequestBody = JsonConvert.SerializeObject(_requestBodyDict); + + var videoInfoRequest = new RestRequest("https://m.gifshow.com/rest/wd/photo/info"); + + videoInfoRequest.AddQueryParameter("kpn", "KUAISHOU_VISION"); + videoInfoRequest.AddQueryParameter("captchaToken", ""); + videoInfoRequest.AddQueryParameter("__NS_hxfalcon", + "HUDR_sFnX-DtsD0FXsbDPTXTMP-sk0it4QvcKvw970-3Y9BKuNdZNdSz2-t2IHP3dz5U08BXEKWpxQPN-GUB9srS50qlqmo1ekhckPk6DAhrsl4X3gBO4Or08YWx5z5k5GG0OErQIMjn9z-vcaVIak0LdBXbB7ElchtlD-bUnhQif$HE_5f40d8bdbf1df021ce8515f40018bc5d031514141415c98cb8ae0fcb37f070266b0685158f422ace6f422afc14"); + videoInfoRequest.AddQueryParameter("caver", "2"); + + _videoInfoHeaders["Referer"] = locationResponse.ResponseUri.AbsoluteUri; + + videoInfoRequest.AddHeaders(_videoInfoHeaders); + videoInfoRequest.AddJsonBody(videoInfoRequestBody); + + var videoInfoResponse = client.Execute(videoInfoRequest); + + + if (videoInfoResponse is null || videoInfoResponse.Content is null) + { + throw new InvalidDataException("响应数据为空,请查看你的日志"); + } + + _logger.Information("开始解析匹配到的json {videoJson}", videoInfoResponse.Content); + + // 反序列化JSON字符串为C#对象 + var videoData = JsonConvert.DeserializeObject(videoInfoResponse.Content); + + if (videoData is null) + { + throw new InvalidDataException("JSON解析数据为空,请检查分享链接是否正确,如有更多问题请查看日志"); + } + + + return new VideoModel + { + Platform = ShortVideoPlatformEnum.KuaiShou, + VideoId = videoData.Photo.Manifest.VideoId, + AuthorName = videoData.Photo.UserName, + AuthorAvatar = videoData.Photo.HeadUrl.ToString(), + Title = videoData.Photo.Caption, + Cover = videoData.Photo.CoverUrls.First().Url.ToString(), + VideoUrl = videoData.Mp4Url.ToString(), + Mp3Url = "", + CreatedTime = + DateTimeOffset.FromUnixTimeMilliseconds(videoData.Photo.Timestamp) + .ToString("yyyy-MM-dd HH:mm:ss"), + Desc = "暂无~", + Duration = videoData.Photo.Duration.ToString(), + DiggCount = videoData.Photo.LikeCount, + ViewCount = videoData.Photo.ViewCount, + CollectCount = videoData.Counts.CollectionCount, + CommentCount = videoData.Photo.CommentCount, + ShareCount = videoData.Photo.ShareCount, + }; + } + catch (Exception e) + { + _logger.Error(e.Message); + throw; + } + } + + public async Task DownloadAsync(string url, string savePath, string fileName, EventHandler onProgressChanged, + EventHandler onProgressCompleted) + { + DownloadConfiguration downloadConfiguration = new() + { + ChunkCount = 8, // Download in 8 chunks (increase for larger files) + MaxTryAgainOnFailover = 5, // Retry up to 5 times on failure + Timeout = 10000, // 10 seconds timeout for each request + RequestConfiguration = new RequestConfiguration + { + UserAgent = _videoInfoHeaders.GetValueOrDefault("User-Agent") + } + }; + + DownloadService downloader = new(downloadConfiguration); + + downloader.DownloadProgressChanged += onProgressChanged; + downloader.DownloadFileCompleted += onProgressCompleted; + + try + { + await downloader.DownloadFileTaskAsync(url, savePath + fileName); + } + catch (Exception ex) + { + _logger.Information("Download failed: {ex}", ex); + throw; + } + } + } +} \ No newline at end of file diff --git a/src/BlueCatKoKo.Ui/ViewModels/Pages/HomeViewModel.cs b/src/BlueCatKoKo.Ui/ViewModels/Pages/HomeViewModel.cs index a13f4c3..eadff7c 100644 --- a/src/BlueCatKoKo.Ui/ViewModels/Pages/HomeViewModel.cs +++ b/src/BlueCatKoKo.Ui/ViewModels/Pages/HomeViewModel.cs @@ -3,7 +3,6 @@ using BlueCatKoKo.Ui.Constants; using BlueCatKoKo.Ui.Models; -using BlueCatKoKo.Ui.Models.Pages; using BlueCatKoKo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; @@ -25,7 +24,8 @@ namespace BlueCatKoKo.Ui.ViewModels.Pages public partial class HomeViewModel : ViewModelBase { private readonly IOptions _appConfig; - private readonly IDownloaderService _douyinDownloaderService; + private readonly IShortVideoService _douYinShortVideoService; + private readonly IShortVideoService _kuaiShortVideoService; private readonly ILogger _logger; // 解析出的视频数据 @@ -55,7 +55,8 @@ [ObservableProperty] [Required(ErrorMessage = "缺少分享链接")] [ObservableProperty] private string _isParsed; public HomeViewModel(IMessenger messenger, ILogger logger, - IDownloaderService douyinDownloaderService, + DouYinShortVideoService douYinShortVideoService, + KuaiShouShortVideoService kuaiShortVideoService, IOptions appConfig) { Messenger = messenger; @@ -71,7 +72,8 @@ public HomeViewModel(IMessenger messenger, ILogger logger, _logger = logger; - _douyinDownloaderService = douyinDownloaderService; + _douYinShortVideoService = douYinShortVideoService; + _kuaiShortVideoService = kuaiShortVideoService; _appConfig = appConfig; @@ -104,18 +106,29 @@ private async Task Parse() throw new ValidationException(errorMessage); } - if (!DownloadUrlText.Contains("https://v.douyin.com")) + if (!DownloadUrlText.Contains("https://")) { throw new ValidationException("请输入正确的分享链接"); } - var downloadUrl = await _douyinDownloaderService.ExtractUrlAsync(DownloadUrlText); - var douYinRouterData = await _douyinDownloaderService.ExtractVideoDataAsync(downloadUrl); + if (DownloadUrlText.Contains(ShortVideoPlatformEnum.DouYin.ToString().ToLower())) + { + var downloadUrl = await _douYinShortVideoService.ExtractUrlAsync(DownloadUrlText); + Data = await _douYinShortVideoService.ExtractVideoDataAsync(downloadUrl); + } + else if (DownloadUrlText.Contains(ShortVideoPlatformEnum.KuaiShou.ToString().ToLower())) + { + var downloadUrl = await _kuaiShortVideoService.ExtractUrlAsync(DownloadUrlText); + Data = await _kuaiShortVideoService.ExtractVideoDataAsync(downloadUrl); + } + else + { + throw new ValidationException("暂不支持该平台"); + } + - Data = douYinRouterData; - // 绑定视频 - using Media media = new(LibVlc, new Uri(douYinRouterData.VideoUrl)); + using Media media = new(LibVlc, new Uri(Data.VideoUrl)); // 这里设置选项,防止自动播放 MediaPlayer.Media = media; MediaPlayer.Pause(); @@ -167,17 +180,37 @@ private async Task DownloadAll() throw new InvalidDataException("无效的下载链接"); } - string filename = _appConfig.Value.DownloadPath + Data.VideoId + ".mp4"; - await _douyinDownloaderService.DownloadAsync(Data.VideoUrl, _appConfig.Value.DownloadPath, - Data.VideoId + ".mp4", - (sender, e) => - { - DownloadProcess = e.ProgressPercentage; - }, (sender, e) => - { - DownloadProcess = 100; - message = filename + "下载成功~"; - }); + var filename = _appConfig.Value.DownloadPath + Data.VideoId + ".mp4"; + + switch (Data.Platform) + { + case ShortVideoPlatformEnum.DouYin: + await _douYinShortVideoService.DownloadAsync(Data.VideoUrl, _appConfig.Value.DownloadPath, + Data.VideoId + ".mp4", + (sender, e) => + { + DownloadProcess = e.ProgressPercentage; + }, (sender, e) => + { + DownloadProcess = 100; + message = filename + "下载成功~"; + }); + break; + case ShortVideoPlatformEnum.KuaiShou: + await _kuaiShortVideoService.DownloadAsync(Data.VideoUrl, _appConfig.Value.DownloadPath, + Data.VideoId + ".mp4", + (sender, e) => + { + DownloadProcess = e.ProgressPercentage; + }, (sender, e) => + { + DownloadProcess = 100; + message = filename + "下载成功~"; + }); + break; + default: + throw new ValidationException("暂不支持该平台"); + } } catch (Exception ex) { diff --git a/src/BlueCatKoKo.Ui/Views/Pages/HomePage.xaml b/src/BlueCatKoKo.Ui/Views/Pages/HomePage.xaml index 7d68729..82641cb 100644 --- a/src/BlueCatKoKo.Ui/Views/Pages/HomePage.xaml +++ b/src/BlueCatKoKo.Ui/Views/Pages/HomePage.xaml @@ -21,7 +21,7 @@