[15日目] PythonでYouTube動画ダウンロードソフトを作る

こんにちは、なりかくんと申します。
この記事はなりかくん Advent Calender 2023の15日目の記事です。

今回は、PythonでYouTubeの動画ダウンロードソフトを作っていこうと思います。

# 完成したスクリーンショット
今回最終的にこのようなソフトを作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549951/5086f52c-c98c-9111-65d8-2c6da8bd733a.png)

# ソフトにするために : Flet
今回は、ソフトとして動かしたいのでもちろんGUIで操作できるようにします。
Pythonでは、簡単にGUIアプリを作れるFletというモジュールがあります。

https://flet.dev/

このFletではFlutterベースのGUIアプリを簡単に作ることが出来ます。

# 実際にFletを使ってみよう！
では、実際にFletでソフトを起動してみます。
まず最初にサンプルコードである、カウンターをコピペして起動してみます。

https://flet.dev/docs/#flet-app-example

このようにしっかりとウィンドウが表示されて動かすことが出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549951/bedbd5b8-aff3-60c4-0c95-49c9e0bc1a48.png)

# URLとダウンロードボタンを作る
まず最初に形となるGUIを作ります。シンプルさを求めて、今回はURL（動画ID）とダウンロードボタンのみを操作すればダウンロード出来るようにしました。（作るのがめんどくさかったとかは言ってはいけない）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549951/0dbfdc8d-db72-fa1c-fe21-6f7fe593d59e.png)

また、ダウンロード中の何パーセント終わったかなどの情報が表示されるプログレスバーと保存先が分かりやすいように載せておきます。

このGUIを作るためのコードが以下になります。

```python
import flet as ft
from os.path import expanduser

def main(page: ft.Page):
    page.title = "YouTube動画ダウンロードソフト Created by narikakun"
    page.window_height = 250
    page.window_width = 800

    progressBar = ft.ProgressBar(width=page.window_width - 20, value=0)
    progressText = ft.Text(value="")
    videoUrl = ft.TextField(label="動画URL", value="", prefix_text="https://youtube.com/watch?v=", expand=True)
    downloadButton = ft.FilledButton(text="ダウンロード")
    folderPath = expanduser("~") + "\\Downloads"
    downloadFolder = ft.TextField(label="保存先", value=folderPath, expand=True, read_only=True)
    downloadFolderOpen = ft.FilledButton(text="フォルダーを開く")

    pageColumn = ft.Column(
        [
            ft.Row(
                [
                    videoUrl,
                    downloadButton
                ]
            ),
            progressBar,
            progressText,
            ft.Row(
                [
                    downloadFolder,
                    downloadFolderOpen
                ]
            )
        ],
        alignment=ft.MainAxisAlignment.START,
        scroll=ft.ScrollMode.ALWAYS,
        height=page.window_height - 60
    )
    
    page.add(
        pageColumn
    )

    def page_resize(e):
        pageColumn.height = page.window_height - 60
        progressBar.width = page.window_width - 20
        page.update()

    page.on_resize = page_resize


ft.app(target=main)
```
ウィンドウサイズが変わったときの処理を若干入れてるのがややこしくなっている原因ですね。

# ダウンロード機能を入れる
では、先ほど作ったGUIに実際にダウンロード機能を入れてみます。ダウンロードについては、14日目に扱ったコードを使います。

https://qiita.com/narikakun/items/b6840f22a7ca1c4b6b87

完成したコードが以下のコードです。

```python
from yt_dlp import YoutubeDL
import flet as ft
from os.path import expanduser
import json
import subprocess

def main(page: ft.Page):
    page.title = "YouTube動画ダウンロードソフト Created by narikakun"
    page.window_height = 250
    page.window_width = 800

    def close_dlg(e):
        err_dlg.open = False
        downloadButton.disabled = False
        progressBar.value = 100
        progressText.value = "エラーで終了しました。"
        page.update()

    err_dlg = ft.AlertDialog(
        title=ft.Text("エラー"),
        modal=True,
        content=ft.Text("エラーメッセージ"),
        actions=[
            ft.TextButton("閉じる", on_click=close_dlg),
        ],
        actions_alignment=ft.MainAxisAlignment.END,
    )

    def showError(errMsg):
        err_dlg.content = ft.Text(errMsg)
        page.dialog = err_dlg
        err_dlg.open = True
        page.update()

    def logHook(d):
        if d["status"] == "downloading":
            percent = (d["downloaded_bytes"] / (d["total_bytes"] or 0)) * 100
            progressBar.value = percent
            progressText.value = f"ダウンロード中... {d['_default_template']}"
            progressText.update()
            progressBar.update()
        elif d["status"] == "finished":
            progressBar.value = 100
            progressText.value = "ダウンロード完了"
        else:
            print(d)

    def getMetaData():
        try:
            with YoutubeDL() as ydl:
                res = ydl.extract_info(f"https://youtube.com/watch?v={videoUrl.value}", download=False)
                return res["id"]
        except:
            showError("メタデータ取得エラーが発生しました")

    def videoDownload(e):
        try:
            if videoUrl.value == "":
                showError("動画URLが入力されていません。")
                return
            downloadButton.disabled = True
            progressBar.value = None
            progressText.value = "ダウンロード開始処理中... "
            page.update()
            videoId = getMetaData()
            downloadFolder.value = folderPath + f"\\{videoId}\\"
            downloadFolder.update()
            with YoutubeDL({
                'progress_hooks': [logHook],
                'outtmpl': folderPath + '/%(id)s/video.mp4'
            }) as ydl:
                ydl.download(f"https://youtube.com/watch?v={videoUrl.value}")
                downloadButton.disabled = False
                page.update()
        except:
            showError("ビデオダウンロード中取得エラーが発生しました")

    def folderOpen(e):
        subprocess.Popen(["explorer", rf"{downloadFolder.value}"], shell=True)

    progressBar = ft.ProgressBar(width=page.window_width - 20, value=0)
    progressText = ft.Text(value="")
    videoUrl = ft.TextField(label="動画URL", value="", prefix_text="https://youtube.com/watch?v=", expand=True)
    downloadButton = ft.FilledButton(text="ダウンロード", on_click=videoDownload)
    folderPath = expanduser("~") + "\\Downloads"
    downloadFolder = ft.TextField(label="保存先", value=folderPath, expand=True, read_only=True)
    downloadFolderOpen = ft.FilledButton(text="フォルダーを開く", on_click=folderOpen)

    pageColumn = ft.Column(
        [
            ft.Row(
                [
                    videoUrl,
                    downloadButton
                ]
            ),
            progressBar,
            progressText,
            ft.Row(
                [
                    downloadFolder,
                    downloadFolderOpen
                ]
            )
        ],
        alignment=ft.MainAxisAlignment.START,
        scroll=ft.ScrollMode.ALWAYS,
        height=page.window_height - 60
    )
    
    page.add(
        pageColumn
    )

    def page_resize(e):
        pageColumn.height = page.window_height - 60
        progressBar.width = page.window_width - 20
        page.update()

    page.on_resize = page_resize


ft.app(target=main)
```

実際に動かすとこのようにダウンロードが始まって動画ダウンロードが出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549951/7884f280-34bb-7edb-ddb4-c4f0fb76c266.png)

実際にフォルダーに保存できています☆

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549951/34195ecf-b483-5950-fa97-f6b2d56f4415.png)

# 最後に
最後までお読みいただきありがとうございました。