iPhoneでショートカットappを使って、safariからYouTubeをダウンロードする

# iPhoneでショートカットappを使って、safariからYouTubeをダウンロードする
### 更新履歴
24/11/4
playlistをまるまる全部ダウンロードを追加。
また、内部でダウンロードdirのpathを指定できるように変更
ショートカットの中のdirpathの部分を変更することで、ダウンロードモードごとのディレクトリを変更できる。
元々使ってる人は「スタートアップ,アップデート」を一回やってから使ってね


## あいさつ
こんにちは、この記事が初のpythonを習いたての大学生です。
個人的な興味で題のようなものを作成したくなり、作るにあたってpythonを使用してみました。
使う際は当然自己責任でお願いします。
その中で躓いた点について、同じようなことをしようとしている、同じようなプログラミングレベルの人の役に立てばと思います。
プログラミングのぷの字に触れたばかりの人間ですので、コードが汚いだとか、例外処理がとかはご勘弁を
2023/4/7

## 動作環境について
自分の確認している環境はiphone Xs ios17.6.1 a-shell ver1.15.6です。
それ以外の環境だったりはよくわからないので悪しからず

## どんなことをするのか


## 使用手順
### 1. a-Shellのインストール、設定等
今回の方法では「a-Shell」というアプリを使用します。ので、ダウンロード

https://apps.apple.com/jp/app/a-shell/id1473805438

### 2. 実行ショートカットの作成
以下のショートカットをリックから作成します
※24/11/4更新

https://www.icloud.com/shortcuts/975507aa7aa343b7ab156c8aa5e85bad

その後、youtubeの共有マークから、「Yt-dlp」を押してダウンロードします。

### 3. 使用の際のオプション
的当なWebページで共有をおし、「Yt-dlp for iPhone」を使用。
下の画像のように出るので、「スタートアップ,アップデート」を選択。

### 3. 使用の際のオプション

「Yt-dlp」を使うと下のように選択するようでる。

<img width="300" alt="IMG_2865.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1278266/b1df9976-e4d4-35ed-2b68-acad55314add.png">

- オーディオファイル(mp3)
 いっちゃん音質のいいものをmp3でダウンロード
- オーディオファイル(opus)
 いっちゃん音質のいいものをopusでダウンロード(わからなければ使わない方がいい)
- 720p,mp4(h264,mp4a)
 720pの動画を、30fps以下になるようにmp4でダウンロード
- 最高画質,音質のmp4(h264,mp4a)
 名前の通り、
- 最高画質,音質のmp4(vp9,opus)
 非推奨

サムネイル付きで、Fileアプリの、このiPhone内/a-Shellにダウンロードされます。

「スタートアップ,アップデート」は初回使用時と一個下の状態の時に使用。

### 4. playlistを含むurlでは
`https://www.youtube.com/watch?v=~~&list=~~`のようなurlでは`&list`という部分がplaylist
これを含んでいる際にはplaylistをまるまる全部ダウンロードするかどうかを選べる。

<img width="300" alt="playlist.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1278266/34518210-1859-6373-1f32-04bef8445468.png"/>

ただし、確認が1回入る。
往々にしてplaylistはたくさんの動画を含むので、間違えると大事故になりかねないため、



## youtubeに対策されたのか、動かなくなることがある件について
この前、`HTTP Error 403: Forbidden`とか言って、動かなくなることがありました。
多分ですがyoutubeが拒否していたのかなと思います。
上の「スタートアップ,アップデート」で更新してみてください。
それでダメならyt_dlpがアップデートしてくれるまでちょっと待ってみましょう。

https://github.com/yt-dlp/yt-dlp/issues/7984


# 以下は内容説明なので読む必要ないです

### 「a-Shell」について
iOS上でシンプルなUnixライクな端末を使える。コマンド解釈にios_systemを使用するそう。
cを使ったものに関しては外から入れても動かないようですが、ffmpegに関しては要望が多かったのか、専用にチューニングしたものが最初から入っているようです。ので、今回pythonで使うためにラッパーである「ffmpeg-python」を入れていますが、ffmpeg本体は追加でインストールする必要はないです(.wasmで入れなきゃいけないなどと書かれているのは昔の話です)。
参考:https://github.com/holzschu/a-shell/issues/8#issuecomment-923821182

### pythonで実行するわけ
a-Shellを知り、Yt-dlpで直接スマホでyoutubeをダウンロードできるんじゃ？となりました。
いつもターミナルで実行している文を入れてみると、「--embed-thumbnail」がうまく動作せず、
毎回別れたままになってしまいました。
※「--embed-thumbnail」:サムネイルをyoutubeから取ってきて動画にくっつける
また、youtubeのタイトルをそのまま動画ファイルのタイトルにすると、保存するとき「/」で勝手にホルダーを作ってしまうなどの不便さがありました。
そこで、複数行にわたっての実行を試す際、実行結果の変数への入れ方がどうにも分かりませんでした(`x=$(~~)`は試した)。
ので、pythonの実行ファイルを作って、それをa-shellから実行するという形をとりました。

## メインである`yt_dlp_expand`
2024/10/25より以下から引っ張ってくるようにしました。

https://github.com/souno1218/yt_dlp_expand/tree/main

以下の順で実行しています。
1. playlist付きのurlを複数のurlに分解
2. デバイス情報,パスを絶対パスに
3. Yt_dlpを使ってタイトルの取得
4. Yt_dlpを使ってサムネのダウンロード
5. Yt_dlpで動画orオーディオのダウンロード
6. (audioのみ)サムネを正方形に
7. サムネと動画を合体

### 1. playlist付きのurlを複数のurlに分解
```python
def split_playlist_url(playlist_url):
    script = (
        f"yt-dlp '{playlist_url}' "
        "--print '%(url)s' "
        "--skip-download "
        "--flat-playlist "
        "--no-check-certificate "
        "--no-check-certificate "
        "--extractor-args youtube:lang=ja;player-client=web"
    )
    cp = subprocess.run(script, encoding="utf-8", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
    list_url = []
    for i in cp.stdout.split("\n"):
        if not len(i) == 0:
            list_url.append(i)
    return list_url
```
オプションでplaylistをまるまるダウンロードを追加。
上ので、playlistの中の動画1つ1つのurlに分解して一つずつダウンロードしていく。

https://www.reddit.com/r/youtubedl/comments/zp93q3/quick_way_to_get_url_list_from_playlist/

### 2. デバイス情報,パスを絶対パスに
```python
def __init__(self, DownloadMode, url, path=None):
    self.mode_num = DownloadMode
    self.download_url = url
    self.device_info()
    self.ext = {0: "mp3", 1: "opus", 2: "mp4", 3: "mp4", 4: "mp4"}[self.mode_num]
    if path is None:
        if self.is_pc:
            path = "~/Downloads"
        else:
            path = "~/Documents"
    self.output_path = pathlib.Path(path).expanduser()

def device_info(self):
    os_name = platform.system()
    if not os_name in [
        "Darwin",
        "Linux",
        "Windows",
    ]:
        raise
    self.is_pc = True
    if os_name == "Darwin":
        device = platform.platform().split("-")[2]
        if ("iPhone" in device) or ("iPad" in device):
            self.is_pc = False
```
まず、実行OSを取得しています。
自身ではこのコードをそのままmacでも使用しているので、まあその辺のためです。

次にpathを正確にしています。
よく`~/Downloads`のように書きますが、これが問題になっていそうな部分があったので、
`pathlib.Path(path).expanduser()`の部分でフルパスに直しています。
ちなみに、iphoneのファイルアプリの中では各アプリのフォルダーが並んで見えますが、
実際には各々のアプリで違うところにあるものがエイリアスとしてまとまっています。
また、pathは`os.getcwd()`で見ることができ、
`/private/var/mobile/Containers/Data/Application/A21064C98962G3C1`
のように意味不明な文字列が入り、さらにこれがアプリのアップデートごとに変わるらしい?です。
また、a-shellから他のアプリのファイルにはアクセスできないらしいです。

### 3. Yt_dlpを使ってタイトルの取得

```python
def getTitle(self):
    script = (
        f"yt-dlp '{self.download_url}' "
        "--skip-download "
        "--print 'title' "
        "--no-check-certificate "
        "--no-playlist "
        "--extractor-args youtube:lang=ja;player-client=web"
    )
    cp = subprocess.run(script, encoding="utf-8", 
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
    self.title = cp.stdout.replace("\n", "")
    replaceList = {":": "-", "[": "「", "]": "」", "/": "／", "\n": " ", "'": "’"}
    for key, value in replaceList.items():
        self.title = self.title.replace(key, value)
    print(f"getTitle Done : {self.title}")
```
Yt_dlpを使って(参考①)タイトルの取得を取得して、入ると困る文字列を他の文字に置き換えて(参考②)います。
基本以下以降ではsubprocessを使ってYt_dlpを流しています。
   
参考にしたサイト
①[動画ダウンロードツール youtube-dl のフォークである yt-dlp を使ってみる](https://vlike-vlife.netlify.app/posts/cli_yt-dlp)
②[Pythonで複数パターンを置換する](https://www.kabegiwablog.com/entry/2018/06/25/100000)
③[yt-dlpをPythonから実行する](https://note.com/note_fumi/n/n72b755dac88a)(最終的に使わなかった)


### 4. Yt_dlpを使ってサムネのダウンロード
```python
def download_thumbnail_jpg(self):
    self.thumbnail_path = f"{self.output_path}/{self.title}.jpg"
    script = (
        f"yt-dlp '{self.download_url}' "
        "--no-check-certificate "
        "--no-playlist "
        "--skip-download "
        "--write-thumbnail "
        "--convert-thumbnails jpg "
        f"--output '{self.output_path}/{self.title}'"
    )
    subprocess.run(script, stdout=subprocess.PIPE, 
                   stderr=subprocess.PIPE, shell=True)
    print(f"download_thumbnail_jpg Done")
```
そのままですね
注意 : どうやらmp3にffmpegでサムネをつける際にはwebpではいけないようです。
mp4にはwebpのままで大丈夫なので、何で何だろう、、、

参考にしたサイト
[How can i download thumbnails in png/jpg format?](https://lightrun.com/answers/ytdl-org-youtube-dl-how-can-i-download-thumbnails-in-pngjpg-format-)


### 5. Yt_dlpで動画orオーディオのダウンロード
```python
def download_file(self):
    # 参考:https://vlike-vlife.netlify.app/posts/cli_yt-dl
    script = f"yt-dlp --no-check-certificate --no-playlist '{self.download_url}' "
    match self.mode_num:
        case 0:
            script = (
                f"-o '{self.output_path}/{self.title}.%(ext)s' "
                "-f 'bestaudio' "
                "--extract-audio "
                "--audio-format mp3 "
            )
            self.file_path = f"{self.output_path}/{self.title}.{self.ext}"
        case 1:
            script = (
                f"-o '{self.output_path}/{self.title}.%(ext)s' "
                "-f 'bestaudio[acodec~=opus]' "
                "--extract-audio "
            )
            self.file_path = f"{self.output_path}/{self.title}.{self.ext}"
        case 2:
            script = (
                f"-o '{self.output_path}/{self.title}_before.%(ext)s' "
                "-f \"bestvideo*[height=720][fps<=30][vcodec~='^(avc|h264)']"
                '+bestaudio[acodec~=mp4a]" '
            )
            self.file_path = f"{self.output_path}/{self.title}_before.{self.ext}"
        case 3:
            script = (
                f"-o '{self.output_path}/{self.title}_before.%(ext)s' "
                "-f \"bestvideo*[vcodec~='^(avc|h264)']+bestaudio[acodec~=mp4a]\" "
            )
            self.file_path = f"{self.output_path}/{self.title}_before.{self.ext}"
        case 4:
            script = (
                f"-o '{self.output_path}/{self.title}_before.%(ext)s' "
                "-f 'bestvideo+bestaudio/best' "
                "--merge-output-format mp4"
            )
            self.file_path = f"{self.output_path}/{self.title}_before.{self.ext}"
    subprocess.run(script, stdout=subprocess.PIPE, 
                   stderr=subprocess.PIPE, shell=True)
    print("UseYt_dlp_iphone Done")
```

1.と同じサイトを参考にダウンロード用のものを作ってます。

### 6. (audioのみ)サムネを正方形に
```python
def crop_thumbnail_square(self):
    probe = ffmpeg.probe(self.thumbnail_path)
    width = min(probe["streams"][0]["width"], probe["streams"][0]["height"])
    (
        ffmpeg.input(self.thumbnail_path)
        .filter("crop", width, width)
        .output(self.thumbnail_path)
        .run(overwrite_output=True)
    )
    print("crop_thumbnail_square Done")
```
なんかオーディオファイルのサムネって正方形だよね、
名前の通り。「overwrite_output=True」は上書きokかどうか
ちなみに、pillowを使うのが一般的だと思うのですけど、a-shellでは使えません(多分)。

参考にしたサイト
[ffmpeg-python](https://github.com/kkroening/ffmpeg-python)

### 6. サムネと動画を合体
名前の通り。ここが一番難しい。
ちなみに、yt-dlpの`--embed-thumbnail`が使えればyt-dlpのみで完結可能
以下、mp3,opus,mp4で結合の仕方がかわる。

#### mp3

```python
    def marge_file_thumbnail_mp3(self):
        file = ID3(self.file_path)
        with open(self.thumbnail_path, "rb") as img_file:
            file.add(APIC(encoding=3, mime="image/jpeg", 
                          type=3, desc="Cover", data=img_file.read()))
        file.save(v2_version=3)

        os.remove(self.thumbnail_path)
        print("marge_file_thumbnail_mp3 Done")
```
mp3ではmutagenを使用しました。

参考
[Pythonを利用したMP3カバーアート取得・更新のやり方](https://qiita.com/moshi/items/0fd2cd8c394ffa927239)

#### opus

```python
def marge_file_thumbnail_opus(self):
    pic = Picture()
    f = OggOpus(self.file_path)
    pic.mime = f"image/jpeg"
    with open(self.thumbnail_path, "rb") as thumbfile:
        pic.data = thumbfile.read()
    pic.type = 3  # front cover
    f["METADATA_BLOCK_PICTURE"] = base64.b64encode(pic.write()).decode("ascii")
    f.save()

    os.remove(self.thumbnail_path)
    print("marge_file_thumbnail_opus Done")
```
opusでもmutagenを使用、やり方はyt-dlpの中覗いてそれっぽい部分を持ってきただけです。

参考
[Breadcrumbsyt-dlp/yt_dlp/postprocessor/embedthumbnail.py](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/postprocessor/embedthumbnail.py)

#### mp4
```python
def marge_file_thumbnail_mp4(self):
    video = ffmpeg.input(self.file_path)
    cover = ffmpeg.input(self.thumbnail_path)
    (
        ffmpeg.output(
            video,
            cover,
            f"{self.output_path}/{self.title}.mp4",
            c="copy",
            **{"c:v:1": "mjpeg"},
            **{"disposition:v:1": "attached_pic"},
        )
        .global_args("-map", "0")
        .global_args("-map", "1")
        .global_args("-loglevel", "error")
        .run(overwrite_output=True)
    )
    os.remove(self.thumbnail_path)
    os.remove(self.file_path)
    print("marge_file_thumbnail_mp4 Done")
```
なぜか、inputとoutputは違う名前じゃないといけない??。
`overwrite_output=True`は上書きするかだが、上のような状態なので、意味があるのか、、、
全体を通して使い終わったファイルの削除もやっているが、まあ特に説明はいいかなと

参考
[ffmpeg_python library to add a custom thumbnail to a .mp4 file using ffmpeg?](https://stackoverflow.com/questions/62267923/ffmpeg-python-library-to-add-a-custom-thumbnail-to-a-mp4-file-using-ffmpeg)


### 更新履歴
24/7/14
内容を大幅に変更しました。今まで使用していた人は更新をお願いします。
24/10/25
変更の反映にいちいちショートカットの更新が必要なのはなんか面倒なので、
githubにスクリプトをあげてそっから引っ張ってくるようにしました。
また、mp3でサムネがくっつかない不具合を修正しました。
24/11/4
playlistをまるまる全部ダウンロードを追加。
また、内部でダウンロードdirのpathを指定できるように変更
ショートカットの中のdirpathの部分を変更することで、ダウンロードモードごとのディレクトリを変更できる。
元々使ってる人は「スタートアップ,アップデート」を一回やってから使ってね