PythonとSeleniumとWebDriverとChrome拡張機能でDownload完了判定と待機

## はじめに

[downloads.State](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/State)を利用してのダウンロードの完了判定と待機が出来ないか検討してみた話です。

前半部分はChrome拡張機能でダウンロードに関する情報をPython側に伝達をする処理を構築していきます。
後半部分はPythonとSeleniumとWebDriverでダウンロードの完了判定と待機をする処理を構築していきます。
最終的なコードは[こちら](https://feo52.hatenablog.jp/entry/2022/12/04/080000)を参照くださいませ。

Chrome拡張機能の詳細な解説はしませんので、
Chrome拡張機能の詳細な解説は[他のサイト](https://feo52.hatenablog.jp/entry/2022/10/02/080000#Reference)などで確認くださいませ。

本投稿は備忘録的な意味合いもあるので、
無駄な記載も含まれています。
ご了承ください。

## 環境

PythonとSeleniumは導入済み、および、
ChromeとChromeのWebDriverは導入済みである、
Windows11の環境として本投稿は記載します。

:::note info
情報
[Webdriver Manager for Python](https://pypi.org/project/webdriver-manager/) などを導入している場合は、
Webdriver初期化時の処理は自身の方法に置き換えてください。
:::

## 準備

Windows11で、
window.downloads.stateフォルダをデスクトップに新規作成し、
window.downloads.stateフォルダに以下ファイルを新規作成します。

```console
C:\Users\USERNAME\Desktop\window.downloads.state>systeminfo | findstr "OS.名 システムの種類"
OS 名:         Microsoft Windows 11 Home
システムの種類: x64-based PC
C:\Users\USERNAME\Desktop\window.downloads.state>dir /b
background.js
content-script.js
manifest.json
sample.py
```

:::note info
情報
sample.pyは本投稿の後半部分で使用します。
:::

## Chrome Extension ( manifest version 3 ) hello world

Chrome拡張機能でハローワールドしてみます。

各種ファイルを下記内容にします。

拡張機能を読み込んだ後、
https://chromedriver.chromium.org/downloads
などの適当なサイトを開くと、
上記サイトのDevtoolsのConsoleと、
background.jsのDevtoolsのConsoleに、
Hello, I am xxxxxxx scripts が、
それぞれに出力されます。

```javascript:background.js
console.log('Hello, I am background scripts');
```

```javascript:content-script.js
console.log('Hello, I am content scripts');
```

```json:manifest.json
{
  "manifest_version": 3,
  "name": "DownloadsState",
  "version": "1.0",
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content-script.js"]
    }
  ],
  "permissions": [
    "downloads",
    "storage",
    "tabs"
  ]
}
```

:::note info
情報
拡張機能読込方法は右記参照 ⇒ [Chrome Extension: Load unpacked extension  ](https://feo52.hatenablog.jp/entry/2022/10/02/080000#Link2)
Devtools起動方法は右記参照 ⇒ [Chrome Extension: Inspect background script](https://feo52.hatenablog.jp/entry/2022/10/02/080000#Link2)
:::

:::note info
情報
[Chrome Extension ( manifest version 3 ) hello world](https://feo52.hatenablog.jp/entry/2022/10/02/080000)
:::

## Chrome Extension ( manifest version 3 ) downloads

[ダウンロードに関する全ての情報](https://developer.chrome.com/docs/extensions/reference/downloads/#type-DownloadItem)を、
background.jsで取得して出力してみます。

background.jsを下記内容に書き換えます。

拡張機能を読み込んだ後、
background.jsのDevtoolsのConsoleを開き、
[https://chromedriver.chromium.org/downloads](https://chromedriver.chromium.org/downloads)
などで適当なファイルをダウンロードすると、
background.jsのDevtoolsのConsoleに、
ダウンロードの状況が出力されます。

```diff_javascript:background.js
console.log('Hello, I am background scripts');

+chrome.downloads.onCreated.addListener(async (item) => {
+  const items = await chrome.downloads.search({});
+  console.log('onCreated');
+  console.log(items);
+});
+chrome.downloads.onChanged.addListener(async (delta) => {
+  const items = await chrome.downloads.search({});
+  console.log('onChanged');
+  console.log(items);
+});
```
:::note info
情報
拡張機能読込方法は右記参照 ⇒ [Chrome Extension: Load unpacked extension  ](https://feo52.hatenablog.jp/entry/2022/10/16/080000#Link2)
Devtools起動方法は右記参照 ⇒ [Chrome Extension: Inspect background script](https://feo52.hatenablog.jp/entry/2022/10/16/080000#Link2)
:::

:::note info
情報
[Chrome Extension ( manifest version 3 ) downloads](https://feo52.hatenablog.jp/entry/2022/10/16/080000)
:::

:::note info
情報
[chrome.downloads DownloadItem](https://developer.chrome.com/docs/extensions/reference/downloads/#type-DownloadItem)
:::

## Downloads State

[ダウンロードに関する全ての情報](https://developer.chrome.com/docs/extensions/reference/downloads/#type-DownloadItem)の
[ダウンロードの状態](https://developer.chrome.com/docs/extensions/reference/downloads/#property-DownloadItem-state)から、
[downloads.State](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/State)と同様に以下を取得してみます。

`in_progress`（ダウンロード途中）
`interrupted`（ダウンロード失敗）
`complete`   （ダウンロード完了）
に追加して、
`unknown`（上記以外）※通常起こりえない状態です
`none`（ダウンロードを一度も行なっていない）

ダウンロードが単一のファイルの場合は、
該当のファイルのダウンロードの状態を返します。
ダウンロードが複数のファイルの場合は、
ひとつでもダウンロード途中のものがあれば`in_progress`を、
ひとつでもダウンロード失敗のものがあれば`interrupted`を、
全ファイルダウンロード完了の状態であれば`complete`を返します。

以下参考です。
`l1`は[ダウンロードに関する全ての情報](https://developer.chrome.com/docs/extensions/reference/downloads/#type-DownloadItem)です。
`l2`は`l1`から空のものを除外したものです。
`l1`に恐らく空のものはないので、
`l2`と`l1`の内容は同じなはずです。
`l3`は`state`だけを取得したものです。

background.jsを下記内容に書き換えます。

拡張機能を読み込んだ後、
background.jsのDevtoolsのConsoleを開き、
https://chromedriver.chromium.org/downloads
などで適当なファイルをダウンロードすると、
background.jsのDevtoolsのConsoleに、
ダウンロードの状況が出力されます。

```diff_javascript:background.js
console.log('Hello, I am background scripts');

+const getWindowDownloadsState = (items) => {
+  let state;
+  if (items?.length) {
+    const l1 = items;
+    const l2 = l1.filter((e) => e);
+    const l3 = l2.map((e) => e.state);
+    const stateInProgress = l3.some((e) => e === 'in_progress');
+    const stateInterrupted = l3.some((e) => e === 'interrupted');
+    const stateComplete = l3.every((e) => e === 'complete');
+    state = stateInProgress ? 'in_progress' : state;
+    state = stateInterrupted ? 'interrupted' : state;
+    state = stateComplete ? 'complete' : state;
+    state = state || 'unknown';
+    // console.log(l1);
+    // console.log(l2);
+    // console.log(l3);
+    // console.log(stateInProgress);
+    // console.log(stateInterrupted);
+    // console.log(stateComplete);
+    // console.log(state);
+  } else {
+    state = 'none';
+  }
+  return state;
+};

chrome.downloads.onCreated.addListener(async (item) => {
  const items = await chrome.downloads.search({});
+ const state = getWindowDownloadsState(items);
+ console.log(`onCreated: ${state}`);
- console.log('onCreated');
- console.log(items);
+ // console.log(items);
});
chrome.downloads.onChanged.addListener(async (delta) => {
  const items = await chrome.downloads.search({});
+ const state = getWindowDownloadsState(items);
+ console.log(`onChanged: ${state}`);
- console.log('onChanged');
- console.log(items);
+ // console.log(items);
});
```
:::note info
情報
拡張機能読込方法は右記参照 ⇒ [Chrome Extension: Load unpacked extension  ](https://feo52.hatenablog.jp/entry/2022/10/16/080000#Link2)
Devtools起動方法は右記参照 ⇒ [Chrome Extension: Inspect background script](https://feo52.hatenablog.jp/entry/2022/10/16/080000#Link2)
:::

:::note info
情報
[Chrome Extension ( manifest version 3 ) downloads](https://feo52.hatenablog.jp/entry/2022/10/16/080000)
:::

:::note info
情報
[chrome.downloads DownloadItem](https://developer.chrome.com/docs/extensions/reference/downloads/#type-DownloadItem)
:::
## Chrome Extension ( manifest version 3 ) storage session

background.jsで取得した情報を、
content-script.jsに伝達します。

background.js、および、
content-script.jsを下記内容に書き換えます。

拡張機能を読み込んだ後、
https://chromedriver.chromium.org/downloads
などで適当なファイルをダウンロードすると、
上記サイトのDevtoolsのConsoleに、
ダウンロードの状況が出力されます。

```diff_javascript:background.js
console.log('Hello, I am background scripts');

const getWindowDownloadsState = (items) => {
  let state;
  // 中略
  return state;
};

chrome.downloads.onCreated.addListener(async (item) => {
  const items = await chrome.downloads.search({});
  const state = getWindowDownloadsState(items);
  console.log(`onCreated: ${state}`);
  // console.log(items);
+ await chrome.storage.session.set({ dataWindowDownloadsState: state });
});
chrome.downloads.onChanged.addListener(async (delta) => {
  const items = await chrome.downloads.search({});
  const state = getWindowDownloadsState(items);
  console.log(`onChanged: ${state}`);
  // console.log(items);
+ await chrome.storage.session.set({ dataWindowDownloadsState: state });
});
```

```diff_javascript:content-script.js
console.log('Hello, I am content scripts');

+chrome.storage.onChanged.addListener((objects, strings) => {
+  if (objects?.dataWindowDownloadsState?.newValue) {
+    console.log(`storage.onChanged: ${objects.dataWindowDownloadsState.newValue}`);
+  }
+});
```

:::note info
情報
拡張機能読込方法は右記参照 ⇒ [Chrome Extension: Load unpacked extension  ](https://feo52.hatenablog.jp/entry/2022/10/09/080000#Link2)
Devtools起動方法は右記参照 ⇒ [Chrome Extension: Inspect background script](https://feo52.hatenablog.jp/entry/2022/10/09/080000#Link2)
:::

:::note info
情報
[Chrome Extension ( manifest version 3 ) storage session](https://feo52.hatenablog.jp/entry/2022/10/09/080000)
:::

## Chrome Extension ( manifest version 3 ) message passing

message passing でも伝達してみます。

background.js、および、
content-script.jsを下記内容に書き換えます。

拡張機能を読み込んだ後、
https://chromedriver.chromium.org/downloads
などで適当なファイルをダウンロードすると、
上記サイトのDevtoolsのConsoleに、
ダウンロードの状況が出力されます。

```diff_javascript:background.js
console.log('Hello, I am background scripts');

const getWindowDownloadsState = (items) => {
  let state;
  // 中略
  return state;
};

+const sendMessage = async (key, message) => {
+  const tabs = await chrome.tabs.query({ url: ['http://*/*', 'https://*/*'] });
+  tabs.forEach((tab) => {
+    chrome.tabs.sendMessage(tab.id, { [key]: message });
+  });
+};

chrome.downloads.onCreated.addListener(async (item) => {
  const items = await chrome.downloads.search({});
  const state = getWindowDownloadsState(items);
  console.log(`onCreated: ${state}`);
  // console.log(items);
+ sendMessage('dataWindowDownloadsState', state);
  await chrome.storage.session.set({ dataWindowDownloadsState: state });
});
chrome.downloads.onChanged.addListener(async (delta) => {
  const items = await chrome.downloads.search({});
  const state = getWindowDownloadsState(items);
  console.log(`onChanged: ${state}`);
  // console.log(items);
+ sendMessage('dataWindowDownloadsState', state);
  await chrome.storage.session.set({ dataWindowDownloadsState: state });
});
```

```diff_javascript:content-script.js
console.log('Hello, I am content scripts');

chrome.storage.onChanged.addListener((objects, strings) => {
  if (objects?.dataWindowDownloadsState?.newValue) {
    console.log(`storage.onChanged: ${objects.dataWindowDownloadsState.newValue}`);
  }
});

+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+  if (request?.dataWindowDownloadsState) {
+    console.log(`runtime.onMessage: ${request.dataWindowDownloadsState}`);
+  }
+});
```

:::note info
情報
拡張機能読込方法は右記参照 ⇒ [Chrome Extension: Load unpacked extension  ](https://feo52.hatenablog.jp/entry/2022/10/23/080001#Link2)
Devtools起動方法は右記参照 ⇒ [Chrome Extension: Inspect background script](https://feo52.hatenablog.jp/entry/2022/10/23/080001#Link2)
:::

:::note info
情報
[Chrome Extension ( manifest version 3 ) message passing](https://feo52.hatenablog.jp/entry/2022/10/23/080001)
:::

## Chrome Extension : chrome.tabs.onUpdated.addListener

タブが更新された時に実行される処理を追加しておきます。
主にサイトロード時に実行されます。

background.jsを下記内容に書き換えます。

拡張機能を読み込んだ後、
https://chromedriver.chromium.org/downloads
などの適当なサイトを開くと、
上記サイトのDevtoolsのConsoleに、
上記サイトのロード時にも、
ダウンロードの状況が出力されます。

```diff_javascript:background.js
console.log('Hello, I am background scripts');

const getWindowDownloadsState = (items) => {
  let state;
  // 中略
  return state;
};

const sendMessage = async (key, message) => {
  const tabs = await chrome.tabs.query({ url: ['http://*/*', 'https://*/*'] });
  tabs.forEach((tab) => {
    chrome.tabs.sendMessage(tab.id, { [key]: message });
  });
};

chrome.downloads.onCreated.addListener(async (item) => {
  const items = await chrome.downloads.search({});
  const state = getWindowDownloadsState(items);
  console.log(`onCreated: ${state}`);
  // console.log(items);
  sendMessage('dataWindowDownloadsState', state);
  await chrome.storage.session.set({ dataWindowDownloadsState: state });
});
chrome.downloads.onChanged.addListener(async (delta) => {
  const items = await chrome.downloads.search({});
  const state = getWindowDownloadsState(items);
  console.log(`onChanged: ${state}`);
  // console.log(items);
  sendMessage('dataWindowDownloadsState', state);
  await chrome.storage.session.set({ dataWindowDownloadsState: state });
});

+chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
+  if (changeInfo.status === 'complete') {
+    const items = await chrome.downloads.search({});
+    const state = getWindowDownloadsState(items);
+    console.log(`onUpdated: ${state}`);
+    // console.log(items);
+    sendMessage('dataWindowDownloadsState', state);
+    await chrome.storage.session.set({ dataWindowDownloadsState: state });
+  }
+});
```

:::note info
情報
[chrome.tabs.onUpdated](https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated)
:::

## custom data attributes

background.jsとcontent-script.jsからsample.pyへと直接伝達する方法も無く、
background.jsとcontent-script.jsへとsample.pyから直接伝達する方法も無いので、
ダーティーな方法ですが、
該当サイトの`<body>`のカスタムデータ属性を利用して伝達します。まずは、
該当サイトの`<body>`のカスタムデータ属性にcontent-script.jsから書き込みます。

Chrome Extension ( manifest version 3 ) storage session で追記したコード側で行なってもよいし、
Chrome Extension ( manifest version 3 ) message passing で追記したコード側で行なってもよいのですが、本投稿は、
Chrome Extension ( manifest version 3 ) message passing で追記したコード側でカスタムデータ属性を書き込みます。

content-script.jsを下記内容に書き換えます。

拡張機能を読み込んだ後、
https://chromedriver.chromium.org/downloads
などで適当なファイルをダウンロードすると、
上記サイトのDevtoolsのElementsで、
body要素のカスタムデータ属性に、
ダウンロードの状況が出力されているのが確認できます。

```diff_javascript:content-script.js
console.log('Hello, I am content scripts');

chrome.storage.onChanged.addListener((objects, strings) => {
  if (objects?.dataWindowDownloadsState?.newValue) {
    console.log(`storage.onChanged: ${objects.dataWindowDownloadsState.newValue}`);
+   // document.body.setAttribute('data-window-downloads-state', objects.dataWindowDownloadsState.newValue);
  }
});

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request?.dataWindowDownloadsState) {
    console.log(`runtime.onMessage: ${request.dataWindowDownloadsState}`);
+   document.body.setAttribute('data-window-downloads-state', request.dataWindowDownloadsState);
  }
});
```

:::note info
情報
[Accessing all the window variables of the current tab in chrome extension](https://stackoverflow.com/questions/32644906/)
[Accessing all the window variables of the current tab in chrome extension : answer](https://stackoverflow.com/questions/32644906/#32648801)
[Is there a limit to the length of HTML attributes](https://stackoverflow.com/questions/1496096/)
[Is there a limit to the length of HTML attributes : answer](https://stackoverflow.com/questions/1496096/#1496165)
:::

## PythonとSeleniumとWebDriver

sample.pyを書いていきます。まずは、
PythonとSeleniumとWebDriverでハローワールドなコードです。
[https://chromedriver.chromium.org/downloads](https://chromedriver.chromium.org/downloads)を開くだけです。

```python:sample.py
import datetime
import os
import time

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait


def main():

    print("PRESS ENTER KEY TO BOOT")
    input()

    driver = None

    try:
        driver = None
        driver = webdriver.Chrome()
        driver.get("https://chromedriver.chromium.org/downloads")
        time.sleep(1)
    except Exception as e:
        print(e)
    finally:
        driver = driver.quit() if driver is not None else None

    print()
    print("PRESS ENTER KEY TO EXIT")
    input()


if __name__ == "__main__":
    main()
```

:::note info
情報
[Webdriver Manager for Python](https://pypi.org/project/webdriver-manager/) などを導入している場合は、
Webdriver初期化時の処理は自身の方法に置き換えてください。
:::

## PythonとSeleniumとWebDriverでDownload

PythonとSeleniumとWebDriverで3個ファイルをダウンロードしてみます。
ただし、すぐにquit()をしてしまうので、恐らくダウンロードが完了しません。

```diff_python:sample.py
import datetime
import os
import time

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait


def main():

    print("PRESS ENTER KEY TO BOOT")
    input()

    driver = None

    try:
        driver = None
        driver = webdriver.Chrome()
        driver.get("https://chromedriver.chromium.org/downloads")
+       WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver.storage.googleapis.com')]")))
+       driver.execute_script("""document.querySelector("a[href*='chromedriver.storage.googleapis.com']").setAttribute("target","_self")""")
+       driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver.storage.googleapis.com')]").click()
+       WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_linux64.zip')]")))
+       WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_mac64.zip')]")))
+       WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_win32.zip')]")))
+       driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_linux64.zip')]").click()
+       driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_mac64.zip')]").click()
+       driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_win32.zip')]").click()
        time.sleep(1)
    except Exception as e:
        print(e)
    finally:
        driver = driver.quit() if driver is not None else None

    print()
    print("PRESS ENTER KEY TO EXIT")
    input()


if __name__ == "__main__":
    main()
```

:::note info
情報
[Webdriver Manager for Python](https://pypi.org/project/webdriver-manager/) などを導入している場合は、
Webdriver初期化時の処理は自身の方法に置き換えてください。
:::

## PythonとSeleniumとWebDriverでChrome拡張機能読込

Chrome拡張機能の読み込みの追加です。

```diff_python:sample.py
import datetime
import os
import time

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait


def main():

    print("PRESS ENTER KEY TO BOOT")
    input()

    driver = None

    try:
+       # extension_location = os.getcwd()
+       extension_location = f"{os.getenv('USERPROFILE')}\\Desktop\\window.downloads.state"
+       options = webdriver.ChromeOptions()
+       options.add_argument(f"load-extension={extension_location}")
        driver = None
-       driver = webdriver.Chrome()
+       driver = webdriver.Chrome(options=options)
        driver.get("https://chromedriver.chromium.org/downloads")
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver.storage.googleapis.com')]")))
        driver.execute_script("""document.querySelector("a[href*='chromedriver.storage.googleapis.com']").setAttribute("target","_self")""")
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver.storage.googleapis.com')]").click()
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_linux64.zip')]")))
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_mac64.zip')]")))
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_win32.zip')]")))
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_linux64.zip')]").click()
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_mac64.zip')]").click()
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_win32.zip')]").click()
        time.sleep(1)
    except Exception as e:
        print(e)
    finally:
        driver = driver.quit() if driver is not None else None

    print()
    print("PRESS ENTER KEY TO EXIT")
    input()


if __name__ == "__main__":
    main()
```

:::note info
情報
拡張機能読込方法は右記参照 ⇒ [Chrome Extension: Installing extensions via ChromeDriver](https://feo52.hatenablog.jp/entry/2022/10/02/080000#Link2)
:::

## PythonとSeleniumとWebDriverとChrome拡張機能でDownload完了判定と待機

Download完了判定の処理の追加です。

```diff_python:sample.py
import datetime
import os
import time

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait


+def checking_conditions_download_ready_state_complete(driver):
+    jikan = f"{f'{datetime.datetime.now()}'[:-4]}"
+    state = driver.execute_script("return document.body.getAttribute('data-window-downloads-state')")
+    _ = state == "in_progress" and print(f"{jikan}: download in progress...")
+    _ = state == "interrupted" and print(f"{jikan}: download interrupted...")
+    _ = state == "complete" and print(f"{jikan}: download complete!!")
+    _ = state == "unknown" and print(f"{jikan}: download unknown...")
+    _ = state == "none" and print(f"{jikan}: download none...")
+    _ = state is None and print(f"{jikan}: error...")
+    return bool(state == "complete")


def main():

    print("PRESS ENTER KEY TO BOOT")
    input()

    driver = None

    try:
        # extension_location = os.getcwd()
        extension_location = f"{os.getenv('USERPROFILE')}\\Desktop\\window.downloads.state"
        options = webdriver.ChromeOptions()
        options.add_argument(f"load-extension={extension_location}")
        driver = None
        driver = webdriver.Chrome(options=options)
        driver.get("https://chromedriver.chromium.org/downloads")
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver.storage.googleapis.com')]")))
        driver.execute_script("""document.querySelector("a[href*='chromedriver.storage.googleapis.com']").setAttribute("target","_self")""")
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver.storage.googleapis.com')]").click()
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_linux64.zip')]")))
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_mac64.zip')]")))
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_win32.zip')]")))
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_linux64.zip')]").click()
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_mac64.zip')]").click()
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_win32.zip')]").click()
+       for _ in range(10):
+           if checking_conditions_download_ready_state_complete(driver):
+               break
+           time.sleep(1)
        time.sleep(1)
    except Exception as e:
        print(e)
    finally:
        driver = driver.quit() if driver is not None else None

    print()
    print("PRESS ENTER KEY TO EXIT")
    input()


if __name__ == "__main__":
    main()
```

## PythonとSeleniumとWebDriverとChrome拡張機能でDownload完了判定と待機（関数内関数）

関数内関数を使用するように変更します。

```diff_python:sample.py
import datetime
import os
import time

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait


-def checking_conditions_download_ready_state_complete(driver):
+def checking_conditions_download_ready_state_complete():
+   def _predicate(driver):
        jikan = f"{f'{datetime.datetime.now()}'[:-4]}"
        state = driver.execute_script("return document.body.getAttribute('data-window-downloads-state')")
        _ = state == "in_progress" and print(f"{jikan}: download in progress...")
        _ = state == "interrupted" and print(f"{jikan}: download interrupted...")
        _ = state == "complete" and print(f"{jikan}: download complete!!")
        _ = state == "unknown" and print(f"{jikan}: download unknown...")
        _ = state == "none" and print(f"{jikan}: download none...")
        _ = state is None and print(f"{jikan}: error...")
        return bool(state == "complete")

+   return _predicate


def main():

    print("PRESS ENTER KEY TO BOOT")
    input()

    driver = None

    try:
        # extension_location = os.getcwd()
        extension_location = f"{os.getenv('USERPROFILE')}\\Desktop\\window.downloads.state"
        options = webdriver.ChromeOptions()
        options.add_argument(f"load-extension={extension_location}")
        driver = None
        driver = webdriver.Chrome(options=options)
        driver.get("https://chromedriver.chromium.org/downloads")
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver.storage.googleapis.com')]")))
        driver.execute_script("""document.querySelector("a[href*='chromedriver.storage.googleapis.com']").setAttribute("target","_self")""")
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver.storage.googleapis.com')]").click()
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_linux64.zip')]")))
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_mac64.zip')]")))
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_win32.zip')]")))
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_linux64.zip')]").click()
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_mac64.zip')]").click()
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_win32.zip')]").click()
        for _ in range(10):
-           if checking_conditions_download_ready_state_complete(driver):
+           if checking_conditions_download_ready_state_complete()(driver):
                break
            time.sleep(1)
        time.sleep(1)
    except Exception as e:
        print(e)
    finally:
        driver = driver.quit() if driver is not None else None

    print()
    print("PRESS ENTER KEY TO EXIT")
    input()


if __name__ == "__main__":
    main()
```

## PythonとSeleniumとWebDriverとChrome拡張機能でDownload完了判定とWebDriverWait().until()による待機

待機の処理をWebDriverWait().until()を使用するように変更します。

```diff_python:sample.py
import datetime
import os
import time

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait


-def checking_conditions_download_ready_state_complete():
+def expected_conditions_download_ready_state_complete():
    def _predicate(driver):
        jikan = f"{f'{datetime.datetime.now()}'[:-4]}"
        state = driver.execute_script("return document.body.getAttribute('data-window-downloads-state')")
        _ = state == "in_progress" and print(f"{jikan}: download in progress...")
        _ = state == "interrupted" and print(f"{jikan}: download interrupted...")
        _ = state == "complete" and print(f"{jikan}: download complete!!")
        _ = state == "unknown" and print(f"{jikan}: download unknown...")
        _ = state == "none" and print(f"{jikan}: download none...")
        _ = state is None and print(f"{jikan}: error...")
        return bool(state == "complete")

    return _predicate


def main():

    print("PRESS ENTER KEY TO BOOT")
    input()

    driver = None

    try:
        # extension_location = os.getcwd()
        extension_location = f"{os.getenv('USERPROFILE')}\\Desktop\\window.downloads.state"
        options = webdriver.ChromeOptions()
        options.add_argument(f"load-extension={extension_location}")
        driver = None
        driver = webdriver.Chrome(options=options)
        driver.get("https://chromedriver.chromium.org/downloads")
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver.storage.googleapis.com')]")))
        driver.execute_script("""document.querySelector("a[href*='chromedriver.storage.googleapis.com']").setAttribute("target","_self")""")
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver.storage.googleapis.com')]").click()
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_linux64.zip')]")))
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_mac64.zip')]")))
        WebDriverWait(driver, 10).until(expected_conditions.element_to_be_clickable((By.XPATH, "//a[contains(@href,'chromedriver_win32.zip')]")))
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_linux64.zip')]").click()
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_mac64.zip')]").click()
        driver.find_element(by=By.XPATH, value="//a[contains(@href,'chromedriver_win32.zip')]").click()
-       for _ in range(10):
-           if checking_conditions_download_ready_state_complete()(driver):
-               break
-           time.sleep(1)
+       WebDriverWait(driver, 10).until(expected_conditions_download_ready_state_complete())
        time.sleep(1)
    except Exception as e:
        print(e)
    finally:
        driver = driver.quit() if driver is not None else None

    print()
    print("PRESS ENTER KEY TO EXIT")
    input()


if __name__ == "__main__":
    main()
```

## おわりに

[downloads.State](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/State)を利用してのダウンロードの完了判定と待機は上記で完成です。
最終的なコードは[こちら](https://feo52.hatenablog.jp/entry/2022/12/04/080000)を参照くださいませ。

~~ただ実は、
Chrome拡張機能のダウンロードに関する情報がダウンロード完了(complete)になった瞬間というのは、
Chrome拡張機能の現状の実装ですとダウンロードファイルの終端(EOF)を取得完了した瞬間のようで、
つまり、ダウンロードファイルの拡張子がリネームされる前のようなのです。~~

```diff_python:sample.py
        WebDriverWait(driver, 10).until(expected_conditions_download_ready_state_complete())
        time.sleep(1)
```

```diff_python:sample.py
        WebDriverWait(driver, 10).until(expected_conditions_download_ready_state_complete())
        driver.quit()
```

~~上段の記載の部分を、
下段の記載に変更して、
ダウンロード完了判定直後にブラウザを閉じるようにすると、
ダウンロードファイルの拡張子がリネームされる前の状態で終了してしまいます。ですので、
ダウンロードファイルの拡張子のリネームをChromeが完了するまでの時間の待機が必要です。
`time.sleep(1)`はそのためのコードです。
ただし、
ダウンロードファイルが多いとかの場合、
`time.sleep(?)`の指定した時間内に終わらないかもしれません...~~

Chrome108?で修正されたのかもしれません。
Chrome108?で修正されたのかもしれませんが、
上記の判定と待機も実施をする方法を[投稿](https://qiita.com/feo52/items/bf40f1f7b6f655cf6adb)しました。

:::note info
参考
[PythonとSeleniumとWebDriverでDownload完了簡易判定と待機](https://qiita.com/feo52/items/afb1badd28a10d04a203)
[PythonとSeleniumとWebDriverとChrome拡張機能でDownload完了判定と待機](https://qiita.com/feo52/items/7e95ea37daf05941383c)
[PythonとSeleniumとWebDriverとChrome拡張機能でDownload完了判定と待機（追加）](https://qiita.com/feo52/items/bf40f1f7b6f655cf6adb)
:::

## あとがき

ノンプログラマーの素人が記述をしたコードです。
狭い利用範囲と少ない利用頻度での確認ですので、
記載内容に間違いや勘違いがあるかもしれません。
上記内容を参照の際は自己責任でお願い致します。