From 90bb20e75fdabf453efe6ec71f4bed8249121fdc Mon Sep 17 00:00:00 2001 From: owenchen Date: Tue, 3 Nov 2026 09:19:50 +0800 Subject: [PATCH] ow --- uniapp/Google Chrome/Class1.cs | 51 ++ uniapp/Google Chrome/Form1.Designer.cs | 244 ++++++ uniapp/Google Chrome/Form1.cs | 767 ++++++++++++++++++ uniapp/Google Chrome/Form1.resx | 120 +++ uniapp/Google Chrome/Form2.Designer.cs | 71 ++ uniapp/Google Chrome/Form2.cs | 37 + uniapp/Google Chrome/Form2.resx | 120 +++ uniapp/Google Chrome/Google Chrome.csproj | 47 ++ .../Google Chrome/Google Chrome.csproj.user | 11 + uniapp/Google Chrome/Google Chrome.sln | 25 + uniapp/Google Chrome/Program.cs | 97 +++ .../Properties/Resources.Designer.cs | 133 +++ .../Google Chrome/Properties/Resources.resx | 142 ++++ uniapp/Google Chrome/Resources/element.png | Bin 0 -> 344 bytes .../Resources/element_plus_16.png | Bin 0 -> 500 bytes .../Google Chrome/Resources/media_sniffer.js | 144 ++++ uniapp/Google Chrome/Resources/ok_16.png | Bin 0 -> 630 bytes uniapp/Google Chrome/Resources/page_link.png | Bin 0 -> 830 bytes uniapp/Google Chrome/Resources/save_16.png | Bin 0 -> 816 bytes uniapp/Google Chrome/Resources/search_16.png | Bin 0 -> 644 bytes .../Google Chrome/Resources/video_button.js | 80 ++ uniapp/Google Chrome/Resources/video_grab.js | 60 ++ uniapp/Google Chrome/Resources/view_16.png | Bin 0 -> 759 bytes uniapp/Google Chrome/VideoDownloader.cs | 573 +++++++++++++ 24 files changed, 2722 insertions(+) create mode 100644 uniapp/Google Chrome/Class1.cs create mode 100644 uniapp/Google Chrome/Form1.Designer.cs create mode 100644 uniapp/Google Chrome/Form1.cs create mode 100644 uniapp/Google Chrome/Form1.resx create mode 100644 uniapp/Google Chrome/Form2.Designer.cs create mode 100644 uniapp/Google Chrome/Form2.cs create mode 100644 uniapp/Google Chrome/Form2.resx create mode 100644 uniapp/Google Chrome/Google Chrome.csproj create mode 100644 uniapp/Google Chrome/Google Chrome.csproj.user create mode 100644 uniapp/Google Chrome/Google Chrome.sln create mode 100644 uniapp/Google Chrome/Program.cs create mode 100644 uniapp/Google Chrome/Properties/Resources.Designer.cs create mode 100644 uniapp/Google Chrome/Properties/Resources.resx create mode 100644 uniapp/Google Chrome/Resources/element.png create mode 100644 uniapp/Google Chrome/Resources/element_plus_16.png create mode 100644 uniapp/Google Chrome/Resources/media_sniffer.js create mode 100644 uniapp/Google Chrome/Resources/ok_16.png create mode 100644 uniapp/Google Chrome/Resources/page_link.png create mode 100644 uniapp/Google Chrome/Resources/save_16.png create mode 100644 uniapp/Google Chrome/Resources/search_16.png create mode 100644 uniapp/Google Chrome/Resources/video_button.js create mode 100644 uniapp/Google Chrome/Resources/video_grab.js create mode 100644 uniapp/Google Chrome/Resources/view_16.png create mode 100644 uniapp/Google Chrome/VideoDownloader.cs diff --git a/uniapp/Google Chrome/Class1.cs b/uniapp/Google Chrome/Class1.cs new file mode 100644 index 0000000..46c776a --- /dev/null +++ b/uniapp/Google Chrome/Class1.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WinFormsApp3 +{ + internal class Class1 + { + } +}/* + // 这是正确、最简、不会报错的 NER 辅助类 + public class NerInput + { + public string Text { get; set; } + } + + public class NerOutput + { + // 不需要 Label!不会报错! + public string[] Words { get; set; } + } + + var mlContext = new MLContext(); + + // ============================ + // 关键:只做 分词 + 特征转换 + // 绝对不加分类器,就不会要 Label + // ============================ + var pipeline = mlContext.Transforms.Text.TokenizeIntoWords("Words", nameof(NerInput.Text)); + + // 空数据也不会报错 + var emptyData = mlContext.Data.LoadFromEnumerable(new List()); + var model = pipeline.Fit(emptyData); + + // 预测引擎 + var engine = mlContext.Model.CreatePredictionEngine(model); + + // 测试 + var result = engine.Predict(new NerInput + { + Text = "我和陈明今天去上海见张伟" + }); + + Console.WriteLine("分词结果:"); + foreach (var word in result.Words) + { + Console.WriteLine("- " + word); + } + */ \ No newline at end of file diff --git a/uniapp/Google Chrome/Form1.Designer.cs b/uniapp/Google Chrome/Form1.Designer.cs new file mode 100644 index 0000000..effdf73 --- /dev/null +++ b/uniapp/Google Chrome/Form1.Designer.cs @@ -0,0 +1,244 @@ +namespace WinFormsApp3 +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + tableLayoutPanel1 = new TableLayoutPanel(); + webView21 = new Microsoft.Web.WebView2.WinForms.WebView2(); + tableLayoutPanel2 = new TableLayoutPanel(); + urlTxt = new TextBox(); + goBut = new Button(); + displayBut = new Button(); + cssBut = new Button(); + addLinkBut = new Button(); + linkBut = new Button(); + dlBut = new Button(); + statusStrip1 = new StatusStrip(); + statusLbl = new ToolStripStatusLabel(); + progressBar = new ToolStripProgressBar(); + cancelBut = new ToolStripButton(); + tableLayoutPanel1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)webView21).BeginInit(); + tableLayoutPanel2.SuspendLayout(); + statusStrip1.SuspendLayout(); + SuspendLayout(); + // + // tableLayoutPanel1 + // + tableLayoutPanel1.ColumnCount = 1; + tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 20F)); + tableLayoutPanel1.Controls.Add(webView21, 0, 1); + tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 0); + tableLayoutPanel1.Controls.Add(statusStrip1, 0, 2); + tableLayoutPanel1.Dock = DockStyle.Fill; + tableLayoutPanel1.Location = new Point(0, 0); + tableLayoutPanel1.Name = "tableLayoutPanel1"; + tableLayoutPanel1.RowCount = 3; + tableLayoutPanel1.RowStyles.Add(new RowStyle()); + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); + tableLayoutPanel1.RowStyles.Add(new RowStyle()); + tableLayoutPanel1.Size = new Size(1416, 785); + tableLayoutPanel1.TabIndex = 1; + // + // webView21 + // + webView21.AllowExternalDrop = true; + webView21.DefaultBackgroundColor = Color.White; + webView21.Dock = DockStyle.Fill; + webView21.Location = new Point(3, 38); + webView21.Name = "webView21"; + webView21.Size = new Size(1410, 715); + webView21.TabIndex = 0; + webView21.ZoomFactor = 1D; + // + // tableLayoutPanel2 + // + tableLayoutPanel2.AutoSize = true; + tableLayoutPanel2.ColumnCount = 7; + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle()); + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle()); + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle()); + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle()); + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle()); + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle()); + tableLayoutPanel2.Controls.Add(urlTxt, 0, 0); + tableLayoutPanel2.Controls.Add(goBut, 1, 0); + tableLayoutPanel2.Controls.Add(displayBut, 5, 0); + tableLayoutPanel2.Controls.Add(linkBut, 4, 0); + tableLayoutPanel2.Controls.Add(cssBut, 3, 0); + tableLayoutPanel2.Controls.Add(addLinkBut, 2, 0); + tableLayoutPanel2.Controls.Add(dlBut, 6, 0); + tableLayoutPanel2.Dock = DockStyle.Fill; + tableLayoutPanel2.Location = new Point(3, 3); + tableLayoutPanel2.Name = "tableLayoutPanel2"; + tableLayoutPanel2.RowCount = 1; + tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + tableLayoutPanel2.Size = new Size(1410, 29); + tableLayoutPanel2.TabIndex = 1; + // + // urlTxt + // + urlTxt.Dock = DockStyle.Fill; + urlTxt.Location = new Point(3, 3); + urlTxt.Name = "urlTxt"; + urlTxt.Size = new Size(1134, 23); + urlTxt.TabIndex = 0; + // + // goBut + // + goBut.Image = Google_Chrome.Properties.Resources.ok_16; + goBut.Location = new Point(1143, 3); + goBut.Name = "goBut"; + goBut.Size = new Size(55, 23); + goBut.TabIndex = 1; + goBut.UseVisualStyleBackColor = true; + goBut.Click += goBut_Click; + // + // displayBut + // + displayBut.Image =Google_Chrome.Properties.Resources.view_16; + displayBut.Location = new Point(1387, 3); + displayBut.Name = "displayBut"; + displayBut.Size = new Size(20, 23); + displayBut.TabIndex = 1; + displayBut.UseVisualStyleBackColor = true; + displayBut.Click += displayBut_Click; + // + // cssBut + // + cssBut.Image = Google_Chrome.Properties.Resources.element; + cssBut.Location = new Point(1265, 3); + cssBut.Name = "cssBut"; + cssBut.Size = new Size(55, 23); + cssBut.TabIndex = 1; + cssBut.UseVisualStyleBackColor = true; + cssBut.Click += cssBut_Click; + // + // addLinkBut + // + addLinkBut.Image = Google_Chrome.Properties.Resources.element_plus_16; + addLinkBut.Location = new Point(1204, 3); + addLinkBut.Name = "addLinkBut"; + addLinkBut.Size = new Size(55, 23); + addLinkBut.TabIndex = 1; + addLinkBut.UseVisualStyleBackColor = true; + addLinkBut.Click += addLinkBut_Click; + // + // linkBut + // + linkBut.Image = Google_Chrome.Properties.Resources.page_link; + linkBut.Location = new Point(1326, 3); + linkBut.Name = "linkBut"; + linkBut.Size = new Size(55, 23); + linkBut.TabIndex = 1; + linkBut.UseVisualStyleBackColor = true; + linkBut.Click += linkBut_Click; + // + // dlBut + // + dlBut.Image = Google_Chrome.Properties.Resources.save_16; + dlBut.Location = new Point(1382, 3); + dlBut.Name = "dlBut"; + dlBut.Size = new Size(75, 23); + dlBut.TabIndex = 1; + dlBut.Text = "下载"; + dlBut.TextImageRelation = TextImageRelation.ImageBeforeText; + dlBut.UseVisualStyleBackColor = true; + dlBut.Click += dlBut_Click; + // + // statusStrip1 + // + statusStrip1.Items.AddRange(new ToolStripItem[] { statusLbl, progressBar, cancelBut }); + statusStrip1.Location = new Point(0, 759); + statusStrip1.Name = "statusStrip1"; + statusStrip1.Size = new Size(1410, 26); + statusStrip1.TabIndex = 2; + // + // statusLbl + // + statusLbl.Name = "statusLbl"; + statusLbl.Size = new Size(1080, 21); + statusLbl.Spring = true; + statusLbl.Text = "就绪"; + statusLbl.TextAlign = ContentAlignment.MiddleLeft; + // + // progressBar + // + progressBar.Name = "progressBar"; + progressBar.Size = new Size(200, 18); + progressBar.Visible = false; + // + // cancelBut + // + cancelBut.DisplayStyle = ToolStripItemDisplayStyle.Text; + cancelBut.Enabled = false; + cancelBut.Name = "cancelBut"; + cancelBut.Size = new Size(60, 23); + cancelBut.Text = "取消下载"; + cancelBut.Click += cancelBut_Click; + // + // Form1 + // + AutoScaleDimensions = new SizeF(7F, 17F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1416, 785); + Controls.Add(tableLayoutPanel1); + Name = "Form1"; + Text = "在线"; + FormClosing += Form1_FormClosing; + Load += Form1_Load; + tableLayoutPanel1.ResumeLayout(false); + tableLayoutPanel1.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)webView21).EndInit(); + tableLayoutPanel2.ResumeLayout(false); + tableLayoutPanel2.PerformLayout(); + statusStrip1.ResumeLayout(false); + statusStrip1.PerformLayout(); + ResumeLayout(false); + } + + #endregion + + private TableLayoutPanel tableLayoutPanel1; + private Microsoft.Web.WebView2.WinForms.WebView2 webView21; + private TableLayoutPanel tableLayoutPanel2; + private TextBox urlTxt; + private Button goBut; + private Button displayBut; + private Button cssBut; + private Button addLinkBut; + private Button linkBut; + private Button dlBut; + private StatusStrip statusStrip1; + private ToolStripStatusLabel statusLbl; + private ToolStripProgressBar progressBar; + private ToolStripButton cancelBut; + } +} diff --git a/uniapp/Google Chrome/Form1.cs b/uniapp/Google Chrome/Form1.cs new file mode 100644 index 0000000..6fbfd76 --- /dev/null +++ b/uniapp/Google Chrome/Form1.cs @@ -0,0 +1,767 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Web.WebView2.Core; + +namespace WinFormsApp3 +{ + /// + /// 当前正在播放的视频地址。 + /// + /// 可直接 HTTP 访问的视频地址;PlaylistText 不为空时作为相对地址基准。 + /// 只在内存里拿到 m3u8 文本时使用(抖音这类 MSE 播放)。 + /// 发出这个请求的文档地址;跨域 iframe 播放器必须用它,不然会被防盗链拒掉。 + public record VideoCandidate(string Url, string? PlaylistText = null, string? Referer = null); + + public partial class Form1 : Form + { + private const string SnifferScript = @"Resources\media_sniffer.js"; + private const string ButtonScript = @"Resources\video_button.js"; + private const string GrabScript = @"Resources\video_grab.js"; + + private double currentZoomFactor = 1.3; + private readonly List sniffedUrls = new(); + private readonly List sniffedPlaylists = new(); + + /// 视频地址 → 发出请求的文档地址,用作下载时的 Referer。 + private readonly Dictionary urlReferers = new(); + private readonly Dictionary injectedScripts = new(); + private CancellationTokenSource? downloadCts; + private bool hooksAttached; + + public Form1() + { + InitializeComponent(); + + string css = @" + .pt-top-nav { display: none !important; } + .pt-head-bg { display: none !important; } + .pt-navbg { display: none !important; } + .erwm { display: none !important; } + .pt-right { display: none !important; } + .pt-copyright { display: none !important; } + .pt-crumb { display: none !important; } + .txtinfo { display: none !important; } + .mys-wrapper { display: none !important; } + .tools { display: none !important; } + .headbox { display: none !important; } + .setbox { display: none !important; } + .foot { display: none !important; } + "; + + if (File.Exists("css.txt")) + { + css = File.ReadAllText("css.txt"); + } + + webView21.NavigationCompleted += async (sender, e) => + { + if (e.IsSuccess) + { + await InjectCssAsync(css); + } + }; + + urlTxt.Text = "http://www.luoqiuzww.cc"; + } + + private async Task InjectCssAsync(string css) + { + if (File.Exists("css.txt")) + { + css = File.ReadAllText("css.txt"); + } + + string script = $@" + var style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = `{css}`; + document.head.appendChild(style); + "; + + await webView21.CoreWebView2.ExecuteScriptAsync(script); + } + + private void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e) + { + if (e.IsSuccess) + { + urlTxt.Text = ((Microsoft.Web.WebView2.Core.CoreWebView2)sender).Source; + // 小延迟确保DOM完全加载 + Task.Delay(100).ContinueWith(_ => + { + this.Invoke((MethodInvoker)delegate + { + if (Math.Abs(webView21.ZoomFactor - currentZoomFactor) > 0.01f) + { + webView21.ZoomFactor = currentZoomFactor; + } + }); + }); + } + } + + /// + /// 顶层页面导航完成后重置嗅探结果:只保留当前页面里播过的地址, + /// 免得长时间浏览后下载地址越攒越多。 + /// + private void OnMainNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e) + { + if (!e.IsSuccess) + { + return; + } + + lock (sniffedUrls) sniffedUrls.Clear(); + lock (urlReferers) urlReferers.Clear(); + sniffedPlaylists.Clear(); + } + + private async Task EnsureCoreAsync() + { + if (webView21.CoreWebView2 != null) + { + return; + } + + try + { + // 初始化 CoreWebView2 + await webView21.EnsureCoreWebView2Async(null); + + if (!hooksAttached && webView21.CoreWebView2 is { } core) + { + hooksAttached = true; + + core.FrameNavigationCompleted += OnNavigationCompleted; + core.NavigationCompleted += OnNavigationCompleted; + // 换了页面就把上一页嗅探到的地址清空,下载时只会拿到当前这页在播的视频 + core.NavigationCompleted += OnMainNavigationCompleted; + core.DOMContentLoaded += (sender, e) => webView21.ZoomFactor = currentZoomFactor; + webView21.ZoomFactorChanged += (sender, e) => currentZoomFactor = webView21.ZoomFactor; + + // 页面里的嗅探脚本靠这两个事件把视频地址回传 + core.WebMessageReceived += OnWebMessageReceived; + // 站点自带的"下载"按钮(如抖音)也统一走另存为,避免存到看不见的默认目录 + core.DownloadStarting += OnDownloadStarting; + // 新窗口就地导航,保证嗅探脚本仍然在跑 + core.NewWindowRequested += OnNewWindowRequested; + } + + await InjectScriptsAsync(); + } + catch (Exception ex) + { + MessageBox.Show($"初始化 WebView2 失败: {ex.Message}"); + } + } + + /// + /// 在文档创建阶段注入嗅探与悬浮按钮脚本,保证早于播放器脚本运行(抖音视频是翻页加载的,尤其需要早注入)。 + /// + private async Task InjectScriptsAsync() + { + var core = webView21.CoreWebView2; + if (core == null) + { + return; + } + + foreach (var file in new[] { SnifferScript, ButtonScript }) + { + if (injectedScripts.ContainsKey(file)) + { + continue; + } + + string code = ReadScript(file); + if (string.IsNullOrEmpty(code)) + { + continue; + } + + string id = await core.AddScriptToExecuteOnDocumentCreatedAsync(code); + if (!string.IsNullOrEmpty(id)) + { + injectedScripts[file] = id; + } + } + } + + private async void InitializeWebView() + { + await EnsureCoreAsync(); + + if (webView21.CoreWebView2 == null) + { + return; + } + + try + { + // 加载网页 + webView21.CoreWebView2.Navigate(urlTxt.Text); + } + catch (Exception ex) + { + MessageBox.Show($"加载页面失败: {ex.Message}"); + } + } + + private void Form1_Load(object sender, EventArgs e) + { + if (File.Exists("currentLink.txt")) + { + urlTxt.Text = File.ReadAllText("currentLink.txt"); + } + + // 打开就直接加载页面,不用先点一次“转到”才能看视频 + InitializeWebView(); + } + + private void Form1_FormClosing(object sender, FormClosingEventArgs e) + { + File.WriteAllText("currentLink.txt", urlTxt.Text); + } + + private void cssBut_Click(object sender, EventArgs e) + { + ShowDialog("css.txt"); + } + + private void addLinkBut_Click(object sender, EventArgs e) + { + File.AppendAllText("link.txt", "\r\n" + urlTxt.Text); + } + + private void linkBut_Click(object sender, EventArgs e) + { + ShowDialog("link.txt"); + } + + private void ShowDialog(string path) + { + Form2 form2 = new Form2(); + form2.Path = path; + form2.ShowDialog(); + } + + private void displayBut_Click(object sender, EventArgs e) + { + urlTxt.Visible = !urlTxt.Visible; + } + + private void goBut_Click(object sender, EventArgs e) + { + InitializeWebView(); + } + + #region 视频下载 + + private static string ReadScript(string file) + { + try + { + string path = Path.IsPathRooted(file) ? file : Path.Combine(Application.StartupPath, file); + return File.Exists(path) ? File.ReadAllText(path) : string.Empty; + } + catch + { + return string.Empty; + } + } + + private void SetStatus(string text) + { + if (IsHandleCreated && InvokeRequired) + { + BeginInvoke(new Action(() => statusLbl.Text = text)); + } + else + { + statusLbl.Text = text; + } + } + + /// + /// 嗅探到的地址可能来自不同 iframe,列表要加锁并限制长度, + /// 只留最近若干个,供“当前播放地址取不到时”兜底用。 + /// + private void AddSniffedUrl(string url, string? referer) + { + lock (sniffedUrls) + { + if (!sniffedUrls.Contains(url)) + { + sniffedUrls.Add(url); + } + + while (sniffedUrls.Count > 50) sniffedUrls.RemoveAt(0); + } + + if (!string.IsNullOrWhiteSpace(referer)) + { + lock (urlReferers) urlReferers[url] = referer; + } + } + + private string? RefererFor(string url, string? fallback) + { + lock (urlReferers) + { + if (urlReferers.TryGetValue(url, out var referer)) return referer; + } + + return string.IsNullOrWhiteSpace(fallback) ? null : fallback; + } + + private List SnapshotSniffedUrls() + { + lock (sniffedUrls) return sniffedUrls.ToList(); + } + + /// + /// 接收页面嗅探脚本回传的视频地址与播放列表文本。 + /// + private void OnWebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs e) + { + try + { + string json = e.TryGetWebMessageAsString(); + JsonNode? node = JsonNode.Parse(json); + string? type = node?["type"]?.GetValue(); + + switch (type) + { + case "media": + { + string? url = node?["url"]?.GetValue(); + string? referer = node?["base"]?.GetValue(); + if (!string.IsNullOrEmpty(url) && VideoDownloader.LooksLikeMedia(url)) + { + AddSniffedUrl(url, referer); + if (VideoDownloader.IsHlsUrl(url)) + { + SetStatus("已嗅探到 m3u8,可点击下载"); + } + } + break; + } + case "hlsText": + { + string? text = node?["text"]?.GetValue(); + if (!string.IsNullOrEmpty(text) && text.Contains("#EXTM3U") && !sniffedPlaylists.Contains(text)) + { + sniffedPlaylists.Add(text); + // 只留最近几份播放列表,够用又不会越积越多 + while (sniffedPlaylists.Count > 10) sniffedPlaylists.RemoveAt(0); + SetStatus("已抓到播放列表,可点击下载"); + } + break; + } + case "downloadClick": + // 播放器右下角的悬浮按钮 + _ = DownloadCurrentVideoAsync(); + break; + } + } + catch + { + // 非本程序约定的消息直接忽略 + } + } + + /// + /// 站点自己发起的下载(例如抖音自带的下载入口)改为用户选择保存位置。 + /// + private void OnDownloadStarting(object? sender, CoreWebView2DownloadStartingEventArgs e) + { + string suggested = Path.GetFileName(e.DownloadOperation.ResultFilePath); + if (string.IsNullOrWhiteSpace(suggested)) + { + suggested = "video.mp4"; + } + + using SaveFileDialog dlg = new SaveFileDialog + { + Title = "保存视频", + FileName = suggested, + Filter = "视频文件|*.mp4;*.ts;*.mkv;*.flv;*.mov|所有文件|*.*" + }; + + if (dlg.ShowDialog(this) != DialogResult.OK) + { + e.Cancel = true; + SetStatus("已取消下载"); + return; + } + + string target = dlg.FileName; + e.Handled = true; + e.ResultFilePath = target; + SetStatus($"正在下载 {Path.GetFileName(target)}"); + + CoreWebView2DownloadOperation operation = e.DownloadOperation; + operation.BytesReceivedChanged += (s, ev) => + { + ulong? total = operation.TotalBytesToReceive; + string totalText = total > 0 ? $" / {VideoDownloader.FormatSize((long)total!.Value)}" : string.Empty; + SetStatus($"下载中 {VideoDownloader.FormatSize(operation.BytesReceived)}{totalText}"); + }; + operation.StateChanged += (s, ev) => + { + switch (operation.State) + { + case CoreWebView2DownloadState.Completed: + SetStatus($"已保存 {target}"); + break; + case CoreWebView2DownloadState.Interrupted: + SetStatus($"下载中断:{operation.InterruptReason}"); + break; + } + }; + } + + private void OnNewWindowRequested(object? sender, CoreWebView2NewWindowRequestedEventArgs e) + { + // 抖音等站点的 window.open 在这里就地导航,避免新窗口没有嗅探脚本 + e.Handled = true; + webView21.CoreWebView2?.Navigate(e.Uri); + } + + private async void dlBut_Click(object sender, EventArgs e) + { + await DownloadCurrentVideoAsync(); + } + + /// + /// 只下载当前正在播放的那一个视频地址,不再列出一堆候选让用户挑。 + /// + private async Task DownloadCurrentVideoAsync() + { + if (webView21.CoreWebView2 == null) + { + SetStatus("页面还没加载好,请稍候"); + return; + } + + if (downloadCts != null) + { + SetStatus("已有下载在进行中,可先点“取消下载”"); + return; + } + + dlBut.Enabled = false; + try + { + SetStatus("正在识别当前视频…"); + string raw = await webView21.CoreWebView2.ExecuteScriptAsync(BuildGrabScript()); + VideoCandidate? candidate = CollectCurrentCandidate(raw); + + if (candidate == null) + { + SetStatus("没识别到视频地址"); + MessageBox.Show(this, + "没有抓到当前正在播放的视频地址。\n\n" + + "常见原因:\r\n1. 视频还没真正开始播放,请先点播放再点本按钮;\r\n" + + "2. 站点用了加密流(DRM),无法直接下载;\r\n" + + "3. 视频在 iframe 里且尚未加载完,稍等一两秒重试。", + "下载视频", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + await DownloadVideoAsync(candidate); + } + catch (OperationCanceledException) + { + SetStatus("已取消"); + } + catch (Exception ex) + { + SetStatus($"识别失败:{ex.Message}"); + MessageBox.Show(this, ex.Message, "下载视频", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + finally + { + dlBut.Enabled = true; + } + } + + /// + /// 抓取脚本兜底版本:资源文件丢失时仍能取到当前 video 标签的播放地址。 + /// + private static string BuildGrabScript() + { + string code = ReadScript(GrabScript); + if (!string.IsNullOrEmpty(code)) + { + return code; + } + + return "(function(){var v=document.querySelector('video');" + + "return JSON.stringify({current:v?(v.currentSrc||v.src):'',title:document.title,pageUrl:location.href," + + "sniffed:(window.__mediaUrls||[]).slice(-60),m3u8Blobs:(window.__m3u8Blobs||[]).slice(-1)});})()"; + } + + /// + /// 只取当前正在播放的那一个视频地址,按三层递降: + /// 1. video 标签自己的播放源(最准确,就是正在播的这个); + /// 2. 抖音这类 blob+MSE 播放、或播放器在 iframe 里取不到源时, + /// 退回用本页最近嗅探到的那一个整片地址(m3u8 或直链,分片不算); + /// 3. 只在内存里拿到播放列表文本时,用页面地址做分片相对地址的基准。 + /// + private VideoCandidate? CollectCurrentCandidate(string grabResultJson) + { + string current = string.Empty; + string pageUrl = string.Empty; + List sniffed = new List(); + + try + { + // ExecuteScriptAsync 返回的是 JSON 编码后的字符串,需要先脱一层引号 + string? inner = JsonSerializer.Deserialize(grabResultJson); + if (!string.IsNullOrEmpty(inner)) + { + JsonNode? node = JsonNode.Parse(inner); + current = node?["current"]?.GetValue() ?? string.Empty; + pageUrl = node?["pageUrl"]?.GetValue() ?? string.Empty; + + if (node?["sniffed"] is JsonArray arr) + { + foreach (JsonNode? item in arr) + { + string? u = item?.GetValue(); + if (!string.IsNullOrEmpty(u)) sniffed.Add(u); + } + } + } + } + catch + { + // 页面没有返回可解析结果时,只用宿主侧嗅探到的地址 + } + + if (VideoDownloader.LooksLikeMedia(current) && !VideoDownloader.IsSegmentUrl(current)) + { + return new VideoCandidate(current, null, pageUrl); + } + + // 嗅探结果里最后一个整片地址最接近“正在播的这个”,前面翻页留下的旧地址和单个分片都不取 + List pool = sniffed.Concat(SnapshotSniffedUrls()) + .Where(VideoDownloader.LooksLikeMedia) + .Distinct() + .ToList(); + + int index = pool.FindLastIndex(u => !VideoDownloader.IsSegmentUrl(u)); + + if (index >= 0) + { + string url = pool[index]; + return new VideoCandidate(url, null, RefererFor(url, pageUrl)); + } + + string? text = sniffedPlaylists.Concat(ReadBlobPlaylists(grabResultJson)) + .Distinct() + .LastOrDefault(t => t.Contains("#EXTM3U")); + + return text == null ? null : new VideoCandidate(pageUrl, text, pageUrl); + } + + private static List ReadBlobPlaylists(string grabResultJson) + { + List result = new List(); + try + { + string? inner = JsonSerializer.Deserialize(grabResultJson); + if (inner != null && JsonNode.Parse(inner)?["m3u8Blobs"] is JsonArray arr) + { + foreach (JsonNode? item in arr) + { + string? text = item?.GetValue(); + if (!string.IsNullOrWhiteSpace(text) && text.Contains("#EXTM3U")) result.Add(text); + } + } + } + catch + { + // 忽略 + } + return result; + } + + /// + /// 下载当前播放的这个地址:先让用户选保存位置,再流式落盘。 + /// 地址只有一个,失败就直接报错误信息,不再翻别的地址重试。 + /// + private async Task DownloadVideoAsync(VideoCandidate candidate) + { + string url = candidate.Url; + VideoDownloader namer = new VideoDownloader(RefererFor(url, webView21.CoreWebView2?.Source)); + + using SaveFileDialog dlg = new SaveFileDialog + { + Title = "下载视频", + FileName = namer.SuggestedFileName(url, PageTitle), + Filter = "视频文件|*.mp4;*.ts;*.mkv;*.flv;*.mov|所有文件|*.*", + AddExtension = true + }; + + if (dlg.ShowDialog(this) != DialogResult.OK) + { + SetStatus("已取消"); + return; + } + + string? failure = await TryDownloadAsync(candidate, dlg.FileName); + if (failure == null) return; + + SetStatus($"下载失败:{failure}"); + MessageBox.Show(this, failure, "下载视频", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + + /// + /// 执行一次下载。返回 null 表示成功或用户主动取消,否则返回错误信息给上层提示。 + /// + private async Task TryDownloadAsync(VideoCandidate candidate, string filePath) + { + VideoDownloader downloader = await CreateDownloaderAsync(candidate); + + downloadCts = new CancellationTokenSource(); + Progress progress = new Progress(ReportProgress); + progressBar.Visible = true; + progressBar.Value = 0; + cancelBut.Enabled = true; + SetStatus($"准备下载 {Path.GetFileName(filePath)}"); + + try + { + DownloadResult result = candidate.PlaylistText != null + ? await downloader.DownloadFromPlaylistTextAsync(candidate.PlaylistText, candidate.Url, filePath, progress, downloadCts.Token) + : await downloader.DownloadAsync(candidate.Url, filePath, progress, downloadCts.Token); + + SetStatus($"已下载 {Path.GetFileName(result.FilePath)}({VideoDownloader.FormatSize(result.Bytes)})"); + + if (MessageBox.Show(this, $"视频已保存到:\n{result.FilePath}\n\n要打开所在文件夹吗?", + "下载完成", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes) + { + OpenFolder(result.FilePath); + } + + return null; + } + catch (OperationCanceledException) + { + // 用户主动取消就别再换地址重试了 + SetStatus("已取消下载"); + return null; + } + catch (Exception ex) + { + DeletePartFiles(filePath); + return ex.Message; + } + finally + { + downloadCts?.Dispose(); + downloadCts = null; + progressBar.Visible = false; + cancelBut.Enabled = false; + } + } + + /// + /// 用候选地址自己的 Referer 和浏览器里的 Cookie 建下载器: + /// 播放器在跨域 iframe 里时,拿顶层页面地址当 Referer 会被防盗链拒掉。 + /// + private async Task CreateDownloaderAsync(VideoCandidate candidate) + { + string? referer = candidate.Referer ?? RefererFor(candidate.Url, webView21.CoreWebView2?.Source); + string? cookie = await GetCookieHeaderAsync(candidate.Url); + return new VideoDownloader(referer, cookie); + } + + private async Task GetCookieHeaderAsync(string url) + { + CoreWebView2? core = webView21.CoreWebView2; + if (core == null || !Uri.TryCreate(url, UriKind.Absolute, out Uri? uri)) return null; + + try + { + var cookies = await core.CookieManager.GetCookiesAsync(uri.ToString()); + string header = string.Join("; ", cookies + .Where(c => !string.IsNullOrEmpty(c.Name)) + .Select(c => $"{c.Name}={c.Value}")); + return header.Length == 0 ? null : header; + } + catch + { + // 拿不到 Cookie 就按匿名请求下载 + return null; + } + } + + private void ReportProgress(DownloadProgress value) + { + SetStatus(value.Message); + + if (value.Ratio != null) + { + progressBar.Value = (int)Math.Clamp(value.Ratio.Value * 100, 0, 100); + } + } + + private void cancelBut_Click(object sender, EventArgs e) + { + try + { + downloadCts?.Cancel(); + SetStatus("正在取消…"); + } + catch (Exception ex) + { + SetStatus($"取消失败:{ex.Message}"); + } + } + + private static void DeletePartFiles(string target) + { + try + { + string part = target + ".part"; + if (File.Exists(part)) File.Delete(part); + } + catch + { + // 残留的 .part 交给下次覆盖 + } + } + + private static void OpenFolder(string filePath) + { + try + { + System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\""); + } + catch + { + // 打不开文件夹不影响下载结果 + } + } + + private string PageTitle + { + get + { + try + { + return webView21.CoreWebView2?.DocumentTitle ?? string.Empty; + } + catch + { + return string.Empty; + } + } + } + + #endregion + } +} diff --git a/uniapp/Google Chrome/Form1.resx b/uniapp/Google Chrome/Form1.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/uniapp/Google Chrome/Form1.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/uniapp/Google Chrome/Form2.Designer.cs b/uniapp/Google Chrome/Form2.Designer.cs new file mode 100644 index 0000000..79a7228 --- /dev/null +++ b/uniapp/Google Chrome/Form2.Designer.cs @@ -0,0 +1,71 @@ +namespace WinFormsApp3 +{ + partial class Form2 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + cssTxt = new RichTextBox(); + saveBut = new Button(); + SuspendLayout(); + // + // cssTxt + // + cssTxt.Location = new Point(12, 12); + cssTxt.Name = "cssTxt"; + cssTxt.Size = new Size(772, 366); + cssTxt.TabIndex = 0; + cssTxt.Text = ""; + // + // saveBut + // + saveBut.Image =Google_Chrome.Properties.Resources.save_16; + saveBut.Location = new Point(699, 395); + saveBut.Name = "saveBut"; + saveBut.Size = new Size(57, 43); + saveBut.TabIndex = 1; + saveBut.UseVisualStyleBackColor = true; + saveBut.Click += saveBut_Click; + // + // Form2 + // + AutoScaleDimensions = new SizeF(7F, 17F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(800, 450); + Controls.Add(saveBut); + Controls.Add(cssTxt); + Name = "Form2"; + Text = "Form2"; + Load += Form2_Load; + ResumeLayout(false); + } + + #endregion + + private RichTextBox cssTxt; + private Button saveBut; + } +} \ No newline at end of file diff --git a/uniapp/Google Chrome/Form2.cs b/uniapp/Google Chrome/Form2.cs new file mode 100644 index 0000000..dd5e2f7 --- /dev/null +++ b/uniapp/Google Chrome/Form2.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace WinFormsApp3 +{ + public partial class Form2 : Form + { + public string Path { get; set; } + + public Form2() + { + InitializeComponent(); + } + + private void Form2_Load(object sender, EventArgs e) + { + if (File.Exists(Path)) + { + cssTxt.Text = File.ReadAllText(Path); + } + } + + private void saveBut_Click(object sender, EventArgs e) + { + File.WriteAllText(Path, cssTxt.Text); + + this.Close(); + } + } +} diff --git a/uniapp/Google Chrome/Form2.resx b/uniapp/Google Chrome/Form2.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/uniapp/Google Chrome/Form2.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/uniapp/Google Chrome/Google Chrome.csproj b/uniapp/Google Chrome/Google Chrome.csproj new file mode 100644 index 0000000..b48ee02 --- /dev/null +++ b/uniapp/Google Chrome/Google Chrome.csproj @@ -0,0 +1,47 @@ + + + + WinExe + net8.0-windows + enable + true + enable + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + \ No newline at end of file diff --git a/uniapp/Google Chrome/Google Chrome.csproj.user b/uniapp/Google Chrome/Google Chrome.csproj.user new file mode 100644 index 0000000..5d5d15b --- /dev/null +++ b/uniapp/Google Chrome/Google Chrome.csproj.user @@ -0,0 +1,11 @@ + + + + + Form + + + Form + + + \ No newline at end of file diff --git a/uniapp/Google Chrome/Google Chrome.sln b/uniapp/Google Chrome/Google Chrome.sln new file mode 100644 index 0000000..4a26a5b --- /dev/null +++ b/uniapp/Google Chrome/Google Chrome.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.11.12328.437 stable +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google Chrome", "Google Chrome.csproj", "{CF401873-0387-4EB8-85E3-9B757EF9EA54}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CF401873-0387-4EB8-85E3-9B757EF9EA54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF401873-0387-4EB8-85E3-9B757EF9EA54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF401873-0387-4EB8-85E3-9B757EF9EA54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF401873-0387-4EB8-85E3-9B757EF9EA54}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0AF3BB1F-186C-4D9D-8E04-1935ECCD5E4A} + EndGlobalSection +EndGlobal diff --git a/uniapp/Google Chrome/Program.cs b/uniapp/Google Chrome/Program.cs new file mode 100644 index 0000000..c36c9ef --- /dev/null +++ b/uniapp/Google Chrome/Program.cs @@ -0,0 +1,97 @@ +using Microsoft.ML; +using Microsoft.ML.Data; +using Microsoft.ML.Transforms.Onnx; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using WinFormsApp3; + +//https://huggingface.co/protectai/gyr66-bert-base-chinese-finetuned-ner-onnx/tree/main +//model.onnx +//vocab.txt + +namespace WinFormsApp3 +{ + class Program + { + [STAThread] // ← 加上这个,99% 立刻解决你的错误 + static int Main(string[] args) + { + // 命令行下载:WinFormsApp3.exe --download <视频地址> [--out 保存路径] [--referer 页面地址] + if (args.Length > 0 && string.Equals(args[0], "--download", StringComparison.OrdinalIgnoreCase)) + { + // 下载丢给线程池线程去跑,不要在 STA 主线程上等待异步,否则 COM 线程模式会冲突 + return Task.Run(() => DownloadCliAsync(args)).GetAwaiter().GetResult(); + } + + ApplicationConfiguration.Initialize(); + + // 这里不能提前 form.Show():那会让 Form.Load 在消息循环开始之前执行, + // WebView2 在没有消息泵时初始化会报 RPC_E_CHANGED_MODE,交给 Run 自己显示窗体 + Application.Run(new Form1()); + return 0; + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AttachConsole(int dwProcessId); + + private const int AttachParentProcess = -1; + + /// + /// 命令行下载入口:不创建窗体、不初始化 WebView2,直接把地址下完就退出,方便脚本化验证。 + /// + private static async Task DownloadCliAsync(string[] args) + { + // WinExe 默认没有控制台,Attach 到父进程的输出才能在终端里看到结果 + AttachConsole(AttachParentProcess); + + string url = args.Length > 1 ? args[1] : string.Empty; + if (string.IsNullOrWhiteSpace(url) || url.StartsWith("--", StringComparison.Ordinal)) + { + Console.Error.WriteLine("用法: WinFormsApp3.exe --download <视频地址> [--out 保存路径] [--referer 页面地址]"); + return 2; + } + + string? referer = GetOption(args, "--referer"); + string? outPath = GetOption(args, "--out"); + if (string.IsNullOrWhiteSpace(outPath)) + { + outPath = Path.Combine(Path.GetTempPath(), new VideoDownloader(referer).SuggestedFileName(url, null)); + } + + using CancellationTokenSource cts = new(TimeSpan.FromMinutes(15)); + Progress progress = new(message => Console.WriteLine(message.Message)); + + try + { + VideoDownloader downloader = new(referer); + DownloadResult result = await downloader.DownloadAsync(url, outPath!, progress, cts.Token); + Console.WriteLine($"OK file={result.FilePath} bytes={result.Bytes} converted={result.Converted}"); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine($"FAIL {ex.GetType().Name}: {ex.Message}"); + return 1; + } + } + + /// + /// 取 --out / --referer 这类"选项 值"形式的参数。 + /// + private static string? GetOption(string[] args, string name) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/uniapp/Google Chrome/Properties/Resources.Designer.cs b/uniapp/Google Chrome/Properties/Resources.Designer.cs new file mode 100644 index 0000000..21eff3b --- /dev/null +++ b/uniapp/Google Chrome/Properties/Resources.Designer.cs @@ -0,0 +1,133 @@ +//------------------------------------------------------------------------------ +// +// 這段程式碼是由工具產生的。 +// 執行階段版本:4.0.30319.42000 +// +// 對這個檔案所做的變更可能會造成錯誤的行為,而且如果重新產生程式碼, +// 變更將會遺失。 +// +//------------------------------------------------------------------------------ + +namespace Google_Chrome.Properties { + using System; + + + /// + /// 用於查詢當地語系化字串等的強類型資源類別。 + /// + // 這個類別是自動產生的,是利用 StronglyTypedResourceBuilder + // 類別透過 ResGen 或 Visual Studio 這類工具。 + // 若要加入或移除成員,請編輯您的 .ResX 檔,然後重新執行 ResGen + // (利用 /str 選項),或重建您的 VS 專案。 + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// 傳回這個類別使用的快取的 ResourceManager 執行個體。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Google_Chrome.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// 覆寫目前執行緒的 CurrentUICulture 屬性,對象是所有 + /// 使用這個強類型資源類別的資源查閱。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// 查詢類型 System.Drawing.Bitmap 的當地語系化資源。 + /// + internal static System.Drawing.Bitmap element { + get { + object obj = ResourceManager.GetObject("element", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// 查詢類型 System.Drawing.Bitmap 的當地語系化資源。 + /// + internal static System.Drawing.Bitmap element_plus_16 { + get { + object obj = ResourceManager.GetObject("element_plus_16", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// 查詢類型 System.Drawing.Bitmap 的當地語系化資源。 + /// + internal static System.Drawing.Bitmap ok_16 { + get { + object obj = ResourceManager.GetObject("ok_16", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// 查詢類型 System.Drawing.Bitmap 的當地語系化資源。 + /// + internal static System.Drawing.Bitmap page_link { + get { + object obj = ResourceManager.GetObject("page_link", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// 查詢類型 System.Drawing.Bitmap 的當地語系化資源。 + /// + internal static System.Drawing.Bitmap save_16 { + get { + object obj = ResourceManager.GetObject("save_16", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// 查詢類型 System.Drawing.Bitmap 的當地語系化資源。 + /// + internal static System.Drawing.Bitmap search_16 { + get { + object obj = ResourceManager.GetObject("search_16", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// 查詢類型 System.Drawing.Bitmap 的當地語系化資源。 + /// + internal static System.Drawing.Bitmap view_16 { + get { + object obj = ResourceManager.GetObject("view_16", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/uniapp/Google Chrome/Properties/Resources.resx b/uniapp/Google Chrome/Properties/Resources.resx new file mode 100644 index 0000000..05e1c8a --- /dev/null +++ b/uniapp/Google Chrome/Properties/Resources.resx @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\view_16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\search_16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\ok_16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\element_plus_16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\page_link.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\element.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\save_16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/uniapp/Google Chrome/Resources/element.png b/uniapp/Google Chrome/Resources/element.png new file mode 100644 index 0000000000000000000000000000000000000000..9747dec888efa1981368fa2cd273ecdb43e6dbfa GIT binary patch literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`FFaiwLn>~)o#36tLovPJjDvvwElvW(-c2S8%K1HL}|zRS$h zZ>r}b?Z>V!Op@KzYHkFlt4#3SsL&*F5$I>_64!_l=lp`oqRjM+5(YR+VWonRfq{jt jk%6J1f~TLee^9W3dv214K%b&NP#c4%tDnm{r-UW|_?(xO literal 0 HcmV?d00001 diff --git a/uniapp/Google Chrome/Resources/element_plus_16.png b/uniapp/Google Chrome/Resources/element_plus_16.png new file mode 100644 index 0000000000000000000000000000000000000000..8233437e9e52b8fb8c9216d1dca93d000f85a388 GIT binary patch literal 500 zcmVCTXa)eo6#lHKzVUL6$ z!m@3Og-a;)CN!@&gd=q4eS$=C%Ru~q0hdV_BF$ju)Foev(n@@_W(0000= 0) return; + window.__m3u8Blobs.push(text); + post({ type: "hlsText", text: text }); + } + + function report(u) { + try { + var full = absUrl(u); + if (!isMediaUrl(full)) return; + if (window.__mediaUrls.indexOf(full) >= 0) return; + window.__mediaUrls.push(full); + // base 是发出这个请求的文档地址:播放器常在跨域 iframe 里, + // 宿主必须用它当 Referer,用顶层页面地址会被防盗链判为非法来源 + post({ type: "media", url: full, base: location.href }); + } catch (e) {} + } + + window.__reportMedia = report; + window.__rememberHlsText = remember; + + // 抖音等站点会用 atob(base64) 把播放地址还原出来,这里顺手抓一份解码结果 + try { + var origAtob = window.atob; + if (origAtob) { + window.atob = function (s) { + var r = origAtob.apply(this, arguments); + try { + if (r && typeof r === "string") { + if (r.indexOf("#EXTM3U") >= 0) remember(r); + else if (isMediaUrl(r)) report(r); + } + } catch (e) {} + return r; + }; + } + } catch (e) {} + + try { + var origFetch = window.fetch; + if (origFetch) { + window.fetch = function (input, init) { + try { + var u = + typeof input === "string" || input instanceof URL + ? input + : input && input.url; + if (u) report(u); + } catch (e) {} + return origFetch.apply(this, arguments); + }; + } + } catch (e) {} + + try { + var xhrOpen = XMLHttpRequest.prototype.open; + var xhrSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.open = function (method, u) { + try { + if (u) report(u); + } catch (e) {} + return xhrOpen.apply(this, arguments); + }; + XMLHttpRequest.prototype.send = function () { + var xhr = this; + try { + xhr.addEventListener("load", function () { + try { + if (xhr.responseType === "" || xhr.responseType === "text") { + remember(xhr.responseText); + } else if ( + xhr.responseType === "blob" && + xhr.response && + xhr.response.text + ) { + xhr.response.text().then(remember); + } + } catch (e) {} + }); + } catch (e) {} + return xhrSend.apply(this, arguments); + }; + } catch (e) {} + + // MSE 播放时 video.src 只能是 blob:,抓不到 http 地址,所以靠上面的分片嗅探兜底; + // 这里额外记录 createObjectURL 的调用,方便识别"确实在用 MSE"。 + try { + var ocu = window.URL && window.URL.createObjectURL; + if (ocu) { + window.URL.createObjectURL = function (obj) { + var u = ocu.apply(this, arguments); + try { + window.__usesMse = true; + if (obj && typeof obj.text === "function") obj.text().then(remember); + } catch (e) {} + return u; + }; + } + } catch (e) {} +})(); diff --git a/uniapp/Google Chrome/Resources/ok_16.png b/uniapp/Google Chrome/Resources/ok_16.png new file mode 100644 index 0000000000000000000000000000000000000000..47dc49dd7976bc8ac8ce528c60f58498e92711dc GIT binary patch literal 630 zcmV-+0*U>JP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn z4jTXf0sBcrK~y+TrITAolVKdjf8Wltk-3^Xuq~*FQ8?_vq#(L*AwfY1-EmLL?iqSELh=ps5$h;|sLb(5tQv=WM_g_~XVNjtdXx4ms&CzdIS|C{&X<@xda z-{*NHQ682snk8se>7oHW<2~GacNaM=s{m=CD=4KR7hePktO=mk>Y0pBvVN7X8j#DE zFdAl?$|(iTk95-P*-B9nxNE&!8x4}k$UgxOKlbtV`w$KCv_%+*0HO>A`^W;FJdZ_|Q8(o0wW zBRs}kT;KV0{6MIo(rz>Ts9W{nf?X+0Qz literal 0 HcmV?d00001 diff --git a/uniapp/Google Chrome/Resources/page_link.png b/uniapp/Google Chrome/Resources/page_link.png new file mode 100644 index 0000000000000000000000000000000000000000..312eab0914ab59271384686255d1be913a6b3add GIT binary patch literal 830 zcmV-E1Ht@>P)VWgGzD=Y79#JI$lhEn`|2MpRa?Bt#-nSD~P0P(mbVe{KrOBoKnSsk>m|ML{6l zBosu@om4j#WzNLRAk+{k1JRvL(MfE&vvbb->v>W{*z*1_uMP}0cIRX*?mz+wk%*#O z%0D-+$B*g1nRkvI+_3E8Pr1NC6@5M&4vWaLCnNlr;lNlr4i91z&)eBGqL{L{GNu;Fof}GS9{gM5BJuH;2QWk8yuOZdB3pGR#s8bd~ zAmt<>3Q=YH$t5YJ5;7@+8Uh6=ktBgY6#6Pa%2F?h910?U8cLT43KAj$Z1*==ra&gILO{WkHfs(--F=bly9l~${z@AT>V$oat!YAD@M zBE0v_F{`g#^wOSP-u~!wvlmXdd*uqFqoZ0^{&nEMDU+=!>({S0wrQhFmoB}Yq1)~0 z^{A)L8Xjzdr(W4_exYO6u3a{4*kIeXZMJUR>Q=ksjW_p!rAwDKYUs90>6Q|C>56o@ zbrSq^Xk7Gq#>dAsn@un`Hz$?w$;Ss`%jV7L%9ShgHFV~C)6M>B`Tp%|nqPc&G*A3| z)Qe+}vT4-x^t1~XE@(6wR;^lPWMo9n*~E!cy~)YRsT2{`?fqeIw-e7N@mOA%UcCIq z_kH(_EK7|>pM*1Wt2^DaDAp|cvp*@(ZZDKpYkKC^?97(`0sb&XTXy7N#sB~S07*qo IM6N<$f;YmCWB>pF literal 0 HcmV?d00001 diff --git a/uniapp/Google Chrome/Resources/save_16.png b/uniapp/Google Chrome/Resources/save_16.png new file mode 100644 index 0000000000000000000000000000000000000000..f9d9eabe049b8ee3e8abb15eb6ea3f52a0877fd6 GIT binary patch literal 816 zcmV-01JC@4P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf0>w#0K~y+Tb(2YG z6j2a{zt`LJY|%-aaWZ4Z7|moEH*i7GLk{umNkIt+9z2T#PhKT>@*oPHyoty}Jg6uh zJc*zlL=guJ;*Lue$4u05y8Bt(F>b*J)x54(?|*;QU!^?H)0f9Dpq0szbdttb58wCE zT4Pxj@pzmd2=K(`7(UZ8b@q=OU||{8uZ(lD`Wm;r%$~#NsXcx`y{Pr9j%S-h3_E0XDsZ=UVO-)g)Rv8)^VnwkN z-^tQ2OKjS6mR-AsBM>9}m`XGI^&6(43H%^B2vcaaS`kn#my6Pfnegc88|*}erdl=+ z5C)p2jpH~dL*aW(zRZ1Qwm!qQt(!PHdW1KxUPM02HnD9-;w>ywM*x=Af*f}vfX&;7 zIdS$hsbrEM0yc4Mo0rqmOo^=>-i;ND(ef;mipYlm8aL5KATNKKe8uv$d+~#L^gmaw zpqPI5j6@xuA7qLI3{13EsmeNCxE6< zG$cqN*CR!IU-LbP;W{?*UUdFT#+a?d0r_GVUEM`i_HBK_+MtQja!>dBLkcDJQfe%_yN zBq1rYV30`#Vw(1JKsK$#0&=0XX_lpZI%FSnjK2zOcwS4yObZ1^n#h$IaB0lOM0bl1 zlQ!=^+I*a}nEB+;XxK7TDh!pn%)hNBp4RlfgQMWhl3JQa2Y%F%QGTHS3(XL!A`@Gm ue9h9_D%xUAE>$n0q{554W>Fjc-|+)Eg99d}g=+Kw0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn z4jTXf0trb(K~y+Tl~cQJ8bK62yDaST#&+QdkWdH>KR`i8MeRsPd<+eugCDVz&v2Xy znpl8PEE?mD!vJ^S~IxOT3KZ@%UzL z*FYW)hnP;MVl5mFBN~k&7KBOL zSe9iN<1wyB*H|u>;&z_aWHNziSr`lkJDCUoiEgoM2U<9WP&kgW4x%CA1KAT^Q1cFp5g=)2mr?>E*(d+e) e&1O+5mGBcGm^N#cQe(LQ0000 120 && r.height > 60 && area > bestArea) { + best = vids[i]; + bestArea = area; + } + } + return best; + } + + function ensureButton() { + var btn = document.getElementById(BTN_ID); + var v = bigVideo(); + + if (!btn && v) { + btn = document.createElement("button"); + btn.id = BTN_ID; + btn.type = "button"; + btn.innerText = "下载视频"; + btn.style.cssText = + "position:fixed;z-index:2147483647;padding:5px 12px;border:0;border-radius:16px;" + + "background:#ff2d55;color:#fff;font-size:13px;line-height:1.4;cursor:pointer;" + + 'box-shadow:0 2px 8px rgba(0,0,0,.35);font-family:system-ui,"Microsoft YaHei",sans-serif;'; + btn.addEventListener( + "click", + function (e) { + e.preventDefault(); + e.stopPropagation(); + post({ type: "downloadClick", pageUrl: location.href }); + }, + true, + ); + document.documentElement.appendChild(btn); + } + + if (btn) { + if (!v) { + btn.style.display = "none"; + return; + } + var r = v.getBoundingClientRect(); + btn.style.display = "block"; + btn.style.left = Math.max(8, r.right - btn.offsetWidth - 12) + "px"; + btn.style.top = Math.max(8, r.bottom - btn.offsetHeight - 12) + "px"; + } + } + + // 播放进度变化时(抖音翻页、切换清晰度)重新定位 + document.addEventListener("timeupdate", ensureButton, true); + window.addEventListener("resize", ensureButton, true); + window.addEventListener("scroll", ensureButton, true); + document.addEventListener("mouseover", ensureButton, true); + + if (!window.__downloadBtnTimer) { + window.__downloadBtnTimer = setInterval(ensureButton, 800); + } + ensureButton(); +})(); diff --git a/uniapp/Google Chrome/Resources/video_grab.js b/uniapp/Google Chrome/Resources/video_grab.js new file mode 100644 index 0000000..1c9505a --- /dev/null +++ b/uniapp/Google Chrome/Resources/video_grab.js @@ -0,0 +1,60 @@ +// 点击"下载视频"时执行:读取当前