HeyCHのブログ

慢性疲労のへいちゃんです

srcがblobで始まっている動画をダウンロードする方法

某Xなんちゃらとかで動画を保存したいってなった場合、Chrome等の検証機能でElementsを見ると、
videoタグのsrcにblob:https://~~~みたいなのが設定されていて、それをブラウザに打ち込んでもダウンロードすることができません。

f:id:HeyCH:20211107234218p:plain
画像はBraveの物です

以下にダウンロードする手順を記載します。
※すべての動画を保存できる保証はありません
※この方法でダウンロードするのが規約的、法律的にアウトな場合があります
※ダウンロードする場合、全て自己責任で行ってください

F12または右クリックメニューの「検証」を押す

こうする事で、デベロッパーツールがブラウザに表示されます。(表示されない場合、ChromeやBraveを使ってください)

Networkタブをクリックする

Networkにはブラウザがダウンロードしたファイル等が表示されます。
※動画をダウンロードする場合「.m3u8ファイル」を見つける必要があります。

リロード&動画を再生する

Networkタブを表示した段階で既に動画を再生していた場合、再生する動画のプレイリストが既に流れているのでリロード(再読み込み)します。
また、動画を再生しないとプレイリストをダウンロードしない(と思われる)ので動画を再生します。

「.m3u8」でフィルターにかけ、必要な「.m3u8ファイル」のURLをゲット

「Filter」の欄に「.m3u8」と入力すると、いくつかの「.m3u8ファイル」が見つかると思います。
どれがダウンロードに必要な「.m3u8ファイル」なのかは、ダウンロードして見てみればわかるかもしれません。

m3u8Tomp4とFFmpegをダウンロードする

FFmpegは検索してダウンロードしてください。
m3u8Tomp4は以下からダウンロードしてください。
GitHub - Hey-CH/m3u8Tomp4: xHamなんちゃら等のm3u8ファイルを取得して、1個のmp4に変換するツール(ffmpegを使うので別途ダウンロードが必要)

URL:に「.m3u8ファイル」のURLを
DIR:に保存先のフォルダパスを
NAME:にファイル名(拡張子.mp4をつけてください)を
ffmpeg:にffmpeg.exeのパスを
入力して、Convertボタンを押すとダウンロードできます。

追記

instagram(他は知らない)で同じように動画をダウンロードしようとしてもできませんでした。
そこでちょっと調べてみたのですが、instagramでは「.m3u8」ではなく、直接「.mp4」をFilterにしてください。
そうすると、「https://~~~.mp4?~~~bytestart=xxx&byteend=yyy」という感じのURLが取得できると思います。
複数出てくると思いますが「yyy」が最大の物を見つけ、「xxx」を「0」にてブラウザにURLを打ち込んでみてください。
instagramにおいては、videoタグのsrcに直接mp4が設定されている場合もあります。

【C#】画像認識自動クリックツール

最近、オトギフロンティアやミストトレインガールズ等のブラウザゲームをやっているんですが
この2つのゲームって事実上無限に継続できるコンテンツがあります。

このようなコンテンツを自動で実行する場合
Windows10標準の自動化ツール等でやろうとすると、
現状の画面がどのような状態なのかわからないので、難しかったりする。

しかし、ゲームのようなボタン1個でも凝っているようなGUIの場合
ボタンの画像を見るだけで、それがどのような状況であるか判別できる場合が多い。

そのため、今回、画面キャプチャからボタン等の画像を見つけ出し、そこを自動でクリックするというツールを作ることにした。
(結論から言うと、めちゃめちゃ使える。オトギで500万くらいしかなかった金が既に1億以上ある。※1回のバトルで5万もらえる※鍵もめちゃめちゃ増えた)

マウスの位置指定とクリック

        public const int MOUSEEVENTF_LEFTDOWN = 0x2;
        public const int MOUSEEVENTF_LEFTUP = 0x4;
        [DllImport("USER32.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern void SetCursorPos(int X, int Y);
        [DllImport("USER32.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo);
        //例
        public static void MouseClick(Point p, bool isRepeat = false) {
            SetCursorPos(point.X, point.Y);
            Thread.Sleep(100);//待機(僕の環境では100ms位必要)

            mouse_event(APIs.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
            mouse_event(APIs.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
        }

画像認識(MatchTemplate)

画像認識にはOpenCvSharp4というライブラリを使用します。
NuGetから「OpenCvSharp4」「OpenCvSharp4.runtime.win」をインストールしましょう。
使い方は以下のような感じで、今回のツールではbasePathにスクリーンショットを、targetPathに設定で指定した画像を入れている感じです。
※TemplateMatchModes.CCoeffNormed以外では全く関係無い所がマッチするため不採用
※new Mat(string path)ではなく、BitmapConverter.ToMat(Bitmap bitmap)みたいなのも有るけど、使ったらエラーになったので不採用(1度保存する必要がある)

    public System.Drawing.Rectangle? GetMatch(string basePath, string targetPath, double threshold = 0.9){
        using(var baseMat = new Mat(basePath))
        using(var targetMat = new Mat(targetPath))
        using(var resultMat = new Mat()){
            Cv2.MatchTemplate(baseMat, targetMat, resultMat, TemplateMatchModes.CCoeffNormed);
            Cv2.Threshold(resultMat, resultMat, threshold, 1.0, ThresholdTypes.Tozero);

            OpenCvSharp.Point minloc, maxloc;
            double minval, maxval;
            Cv2.MinMaxLoc(resultMat, out minval, out maxval, out minloc, out maxloc);
            if(maxval>threshold)
                return new System.Drawing.Rectangle(maxloc.X, maxloc.Y, targetMat.Width, targetMat.Height);
            return null;
        }
    }

【C#】自動シャットダウンツールの作成

前回の続きで、タスクトレイに常駐するアプリに自動シャットダウン等の機能を追加していきます。
heych.hatenablog.com
の続きです。

機能

  • 指定した時間にシャットダウンする
  • シャットダウンする際、起動中のアプリをできるだけ終了する
  • シャットダウンする前に、シャットダウンをキャンセルできる画面を表示する(20秒経過後自動で閉じる)
  • シャットダウンする時刻を設定できる画面を表示する
  • タスクトレイのアイコンの右クリックメニューに機能を設定する

シャットダウンする関数

shutdown.exeを使用してPCをシャットダウンします。コマンドラインオプションは下記リンク参照
shutdown | Microsoft Docs

        static void shutdown() {
            ProcessStartInfo psi = new ProcessStartInfo();
            psi.FileName = "shutdown.exe";
            psi.Arguments = "/s /f";
            psi.UseShellExecute = false;
            psi.CreateNoWindow = true;
            Process.Start(psi);
        }

起動中のアプリをできるだけ終了する関数

ここではメインウィンドウを持っているアプリを閉じる処理を行い、最大5分待機します

        static void waitForCloseWindows() {
            var ignoreProcesses = new[] { "TextInputHost", "ApplicationFrameHost", "SystemSettings" };//何故か起動してないCalcurator(電卓)が出てくるが放置
            var closeList = new List<Process>();
            foreach (var p in Process.GetProcesses().Where(pr=>!ignoreProcesses.Contains(pr.ProcessName))) {
                if (p.MainWindowHandle != IntPtr.Zero && !string.IsNullOrEmpty(p.MainWindowTitle)) {
                    //Debug.WriteLine(p.ProcessName);
                    p.CloseMainWindow();
                    closeList.Add(p);
                }
            }
            //5分待っても終了しない場合kill←killは良くないっぽい
            var st = DateTime.Now;
            while (closeList.Any(p => p.HasExited)) {
                if (DateTime.Now - st > new TimeSpan(0, 5, 0)) break;
                Task.Delay(1000);
            }
            //foreach (var p in closeList) {
            //    if (!p.HasExited) p.Kill();
            //}
        }

設定時刻を記憶する設定の追加

  • ソリューションエクスプローラーのプロジェクト(Shutdowner)を右クリックしてプロパティを表示
  • 設定を選択し、「ここをクリックしてください。」的なところをクリックして設定を追加
  • 「Hours」「Minutes」「SettingTime」をそれぞれ、「string」「string」「System.DateTime」で追加し、「Hours」には「5」「Minutes」には「10」と適当な初期値を入れておく

時刻設定画面の追加

  • ソリューションエクスプローラーのプロジェクトを右クリックし、追加>Windowsフォームを選択(元からあるForm1を使ってもOK)し、「SettingForm」と名前を付ける
f:id:HeyCH:20210710145005p:plain
SettingForm
  • 上図のような感じでデザインし、「Icon」をリソースの画像に設定し、「FormBorderStyle」を「FixedDialog」に設定
  • また、両方のTextBox(時間の方がtextBox1)に「TextChanged」イベントを追加し、OKボタンにも「Click」イベントを追加
        public SettingForm() {
            InitializeComponent();

            textBox1.Text = Properties.Settings.Default.Hours;
            textBox2.Text = Properties.Settings.Default.Minutes;
        }
        private void button1_Click(object sender, EventArgs e) {
            var t0 = DateTime.Now;
            try {
                var t1 = new DateTime(t0.Year, t0.Month, t0.Day, int.Parse(textBox1.Text), int.Parse(textBox2.Text), 0);
                Properties.Settings.Default.Hours = textBox1.Text;
                Properties.Settings.Default.Minutes = textBox2.Text;
                Properties.Settings.Default.SettingTime = DateTime.Now;
                Properties.Settings.Default.Save();
                this.Close();
            } catch {
                MessageBox.Show("Could not set time.");
            }
        }

        Regex r = new Regex("[^\\d]");
        private void textBox1_TextChanged(object sender, EventArgs e) {
            if (r.IsMatch(textBox1.Text)) textBox1.Text = r.Replace(textBox1.Text, "");
            if (string.IsNullOrEmpty(textBox1.Text)) textBox1.Text = "0";
            var val = int.Parse(textBox1.Text);
            if (val < 0) val = 0;
            else if (val > 23) val = 23;
            textBox1.Text = val.ToString();
        }

        private void textBox2_TextChanged(object sender, EventArgs e) {
            if (r.IsMatch(textBox2.Text)) textBox2.Text = r.Replace(textBox2.Text, "");
            if (string.IsNullOrEmpty(textBox2.Text)) textBox2.Text = "0";
            var val = int.Parse(textBox2.Text);
            if (val < 0) val = 0;
            else if (val > 59) val = 59;
            textBox2.Text = val.ToString();
        }

20秒待機してキャンセルされたかどうかを判断する画面の追加

  • 先ほどと同じようにして「ConfirmForm」を追加
f:id:HeyCH:20210710150546p:plain
ConfirmForm
  • 上図のようなデザイン(20sは「label2」というラベル)で、先ほどと同じように「Icon」と「FormBorderStyle」を設定し、Formの「Activated」イベントを追加
  • Cancelボタンに「Click」イベントを追加
        int waitTime = 20;
        public ConfirmForm() {
            InitializeComponent();

            this.DialogResult = DialogResult.Cancel;
        }

        private void button1_Click(object sender, EventArgs e) {
            this.Close();
        }

        private void ConfirmForm_Activated(object sender, EventArgs e) {
            //コンストラクタでやるとカウントが開始されない場合がある
            //20秒待ってCancelボタンを押さなければDialogResult.OKを返す
            Task.Run(async () => {
                for (int t = waitTime; t >= 0; t--) {
                    this.Invoke((MethodInvoker)delegate () { label2.Text = t + "s"; });
                    await Task.Delay(1000);
                }
                this.Invoke((MethodInvoker)delegate () {
                    this.DialogResult = DialogResult.OK;
                    this.Close();
                });
            });
        }

こんな風にしておけば、「ShowDialog」したときの戻り値が、何もしない場合「DialogResult.OK」、キャンセルボタンや×を押した場合「DialogResult.Cancel」となる

終了処理

        static bool executeShutdownProcess() {
            //確認画面表示
            var cform = new ConfirmForm();
            if (cform.ShowDialog() == DialogResult.OK) {
                //メイン画面を持つプロセスの終了
                waitForCloseWindows();
                //Shutdown処理
                shutdown();
                //終了
                return true;
            } else {
                return false;
            }
        }

右クリックメニューの追加

            //右クリックメニュー
            ContextMenuStrip cms = new ContextMenuStrip();
            ToolStripMenuItem tsmi1 = new ToolStripMenuItem("Exit");//終了
            tsmi1.Click += (s, e) => {
                survive = false;
                nicon.Dispose();
                Application.Exit();
            };
            ToolStripMenuItem tsmi2 = new ToolStripMenuItem("Shutdown now!");//今すぐシャットダウン
            tsmi2.Click += (s, e) => {
                executeShutdownProcess();
            };
            ToolStripMenuItem tsmi3 = new ToolStripMenuItem("Setting");//設定画面
            tsmi3.Click += (s, e) => {
                if (sform == null) {
                    sform = new SettingForm();
                    sform.FormClosed += (s, e) => { sform = null; };
                }
                sform.Show();
            };
            cms.Items.Add(tsmi3);
            cms.Items.Add(tsmi2);
            cms.Items.Add(tsmi1);
            nicon.ContextMenuStrip = cms;

シャットダウン待機タスク

            Task.Run(async () => {
                var t0 = DateTime.Now;
                DateTime t1 = new(0);
                while (t1.Year < t0.Year) {
                    try {
                        t1 = new DateTime(t0.Year, t0.Month, t0.Day, int.Parse(Properties.Settings.Default.Hours), int.Parse(Properties.Settings.Default.Minutes), 0);
                    } catch {
                        //設定時間でエラーが出たら設定画面を出す(設定ファイルを直接いじったりしたとき用)
                        if (sform == null) {
                            sform = new SettingForm();
                            sform.FormClosed += (s, e) => { sform = null; };
                        }
                        sform.ShowDialog();
                    }
                }
                while (survive) {
                    t0 = DateTime.Now;
                    //設定時間変更確認
                    var t2= new DateTime(t0.Year, t0.Month, t0.Day, int.Parse(Properties.Settings.Default.Hours), int.Parse(Properties.Settings.Default.Minutes), 0);
                    if (t2.Hour != t1.Hour || t2.Minute != t1.Minute) {
                        t1 = t2;
                        if (Properties.Settings.Default.SettingTime > t1) t1 = t1.AddDays(1);//設定時刻を設定した時刻より早ければ、明日実行する
                    }
                    //設定時間を現在時間が超えていても、1時間以上離れていた場合は明日にする
                    if (t0 > t1 && (t0 - t1) > new TimeSpan(1, 0, 0)) t1 = t1.AddDays(1);
                    //設定時間を超えたらシャットダウン
                    if (t0 >= t1) {
                        if (executeShutdownProcess()) break;
                        else t1 = t1.AddDays(1);
                    }
                    await Task.Delay(60000);
                }
            });

Program.cs全部

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Shutdowner {
    static class Program {
        static NotifyIcon nicon;
        static SettingForm sform;
        static bool survive = true;
        /// <summary>
        ///  The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main() {
            Application.SetHighDpiMode(HighDpiMode.SystemAware);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            //Application.Run(new Form1());

            nicon = new NotifyIcon();
            nicon.Icon = Shutdowner.Properties.Resources.Shutdowner;
            nicon.Text = "Shutdowner";
            nicon.Visible = true;
            //コンテキストメニュークリックしたときにも表示されたので止め
            //nicon.MouseClick += (s,e) => {
            //    if (e.Button == MouseButtons.Left) {
            //        if (sform == null) {//form1をcloseしてもnullにはならないのでイベントでnullにしてやる
            //            sform = new SettingForm();
            //            sform.FormClosed += (s, e) => { sform = null; };
            //        }
            //        sform.Show();
            //    }
            //};

            //右クリックメニュー
            ContextMenuStrip cms = new ContextMenuStrip();
            ToolStripMenuItem tsmi1 = new ToolStripMenuItem("Exit");//終了
            tsmi1.Click += (s, e) => {
                survive = false;
                nicon.Dispose();
                Application.Exit();
            };
            ToolStripMenuItem tsmi2 = new ToolStripMenuItem("Shutdown now!");//今すぐシャットダウン
            tsmi2.Click += (s, e) => {
                executeShutdownProcess();
            };
            ToolStripMenuItem tsmi3 = new ToolStripMenuItem("Setting");//設定画面
            tsmi3.Click += (s, e) => {
                if (sform == null) {
                    sform = new SettingForm();
                    sform.FormClosed += (s, e) => { sform = null; };
                }
                sform.Show();
            };
            cms.Items.Add(tsmi3);
            cms.Items.Add(tsmi2);
            cms.Items.Add(tsmi1);
            nicon.ContextMenuStrip = cms;

            Task.Run(async () => {
                var t0 = DateTime.Now;
                DateTime t1 = new(0);
                while (t1.Year < t0.Year) {
                    try {
                        t1 = new DateTime(t0.Year, t0.Month, t0.Day, int.Parse(Properties.Settings.Default.Hours), int.Parse(Properties.Settings.Default.Minutes), 0);
                    } catch {
                        //設定時間でエラーが出たら設定画面を出す(設定ファイルを直接いじったりしたとき用)
                        if (sform == null) {
                            sform = new SettingForm();
                            sform.FormClosed += (s, e) => { sform = null; };
                        }
                        sform.ShowDialog();
                    }
                }
                while (survive) {
                    t0 = DateTime.Now;
                    //設定時間変更確認
                    var t2= new DateTime(t0.Year, t0.Month, t0.Day, int.Parse(Properties.Settings.Default.Hours), int.Parse(Properties.Settings.Default.Minutes), 0);
                    if (t2.Hour != t1.Hour || t2.Minute != t1.Minute) {
                        t1 = t2;
                        if (Properties.Settings.Default.SettingTime > t1) t1 = t1.AddDays(1);//設定時刻を設定した時刻より早ければ、明日実行する
                    }
                    //設定時間を現在時間が超えていても、1時間以上離れていた場合は明日にする
                    if (t0 > t1 && (t0 - t1) > new TimeSpan(1, 0, 0)) t1 = t1.AddDays(1);
                    //設定時間を超えたらシャットダウン
                    if (t0 >= t1) {
                        if (executeShutdownProcess()) break;
                        else t1 = t1.AddDays(1);
                    }
                    await Task.Delay(60000);
                }
            });

            Application.Run();
        }

        static bool executeShutdownProcess() {
            //確認画面表示
            var cform = new ConfirmForm();
            if (cform.ShowDialog() == DialogResult.OK) {
                //メイン画面を持つプロセスの終了
                waitForCloseWindows();
                //Shutdown処理
                shutdown();
                //終了
                return true;
            } else {
                return false;
            }
        }

        static void waitForCloseWindows() {
            var ignoreProcesses = new[] { "TextInputHost", "ApplicationFrameHost", "SystemSettings" };//何故か起動してないCalcurator(電卓)が出てくるが放置
            var closeList = new List<Process>();
            foreach (var p in Process.GetProcesses().Where(pr=>!ignoreProcesses.Contains(pr.ProcessName))) {
                if (p.MainWindowHandle != IntPtr.Zero && !string.IsNullOrEmpty(p.MainWindowTitle)) {
                    //Debug.WriteLine(p.ProcessName);
                    p.CloseMainWindow();
                    closeList.Add(p);
                }
            }
            //5分待っても終了しない場合kill←killは良くないっぽい
            var st = DateTime.Now;
            while (closeList.Any(p => p.HasExited)) {
                if (DateTime.Now - st > new TimeSpan(0, 5, 0)) break;
                Task.Delay(1000);
            }
            //foreach (var p in closeList) {
            //    if (!p.HasExited) p.Kill();
            //}
        }
        static void shutdown() {
            ProcessStartInfo psi = new ProcessStartInfo();
            psi.FileName = "shutdown.exe";
            psi.Arguments = "/s /f";
            psi.UseShellExecute = false;
            psi.CreateNoWindow = true;
            Process.Start(psi);
        }
    }
}

GitHub - Hey-CH/Shutdowner: 指定した時間にPCをシャットダウンするツール(常駐)