🎥

YouTubeのURLを投げたら字幕だけ取り出してくれるオレオレAPIをつくる

2021/06/24に公開1

https://note.com/shinsukesutou/n/nbe447c764ae1

YouTube字幕を抜き出して必要な情報を自分用に抽出するツールのお手伝いをしています。メインの機能を実現しているのがPython製のオープンソースウェアで、ローカル実行はできるもののWeb上でなんとかできないかな? ということで、頑張ってみました。

youtube-dl をベースに開発する

YouTubeダウンロードではパブリックドメインなソフトウェアである「youtube-dl」がよく使われるようで、こいつがPython製です。

https://qiita.com/ShinsukeSutou/items/7aaf1d4385c3bc2e6ddf

https://qiita.com/ShinsukeSutou/items/c090710b71015e656c4d

特にWindows環境ではDLL周りの準備がたくさん必要でもう壮絶的に大変なようですので、別環境だとどうなるのかも兼ねてこちらでもやってみつつ、どこからでも使えるようにBaaSにデプロイしてAPIとして使えるようにすることを目標とします。

おためし1:適当な動画をダウンロードしてみる

https://github.com/ytdl-org/youtube-dl

実行環境はmacOS Catalinaです。音楽作品っぽいのを試しにダウンロードしてみます。

https://youtu.be/iSsct7423J4

$ cd Desktop
$ git clone https://github.com/ytdl-org/youtube-dl
$ cd youtube-dl
$ python3 -m youtube_dl https://youtu.be/iSsct7423J4
[youtube] iSsct7423J4: Downloading webpage
WARNING: Requested formats are incompatible for merge and will be merged into mkv.
[download] Destination: ウ”ィ”エ”-iSsct7423J4.f137.mp4
[download] 100% of 96.44MiB in 00:03
[download] Destination: ウ”ィ”エ”-iSsct7423J4.f251.webm
[download] 100% of 3.51MiB in 00:00
[ffmpeg] Merging formats into "ウ”ィ”エ”-iSsct7423J4.mkv"
Deleting original file ウ”ィ”エ”-iSsct7423J4.f137.mp4 (pass -k to keep)
Deleting original file ウ”ィ”エ”-iSsct7423J4.f251.webm (pass -k to keep)

すごく久々に動画ダウンロードした気がします。10年ぐらい昔にCraving Explorer使ってた以来かもしれない。

https://www.crav-ing.com

おためし2:字幕だけダウンロードしてみる

https://youtu.be/OagYLOhxf6A

# 字幕ファイルのみ「ビデオID.ja.vtt」という名前でダウンロード
$ python3 -m youtube_dl --sub-lang ja --write-auto-sub --skip-download --sub-format vtt -o '%(id)s' https://youtu.be/OagYLOhxf6A
[youtube] OagYLOhxf6A: Downloading webpage
[info] Writing video subtitles to: OagYLOhxf6A.ja.vtt

# 冒頭だけ見てみる
$ head -n10 OagYLOhxf6A.ja.vtt
WEBVTT
Kind: captions
Language: ja

00:00:00.430 --> 00:00:05.279 align:start position:0%

で<00:00:00.670><c></c><00:00:00.760><c>一生</c><00:00:01.120><c></c><00:00:01.300><c></c><00:00:01.390><c>未知</c><00:00:01.569><c></c><00:00:01.660><c>もの</c><00:00:01.810><c></c><00:00:01.960><c>動く</c><00:00:02.440><c></c><00:00:02.530><c></c><00:00:02.650><c>全幅</c><00:00:02.980><c></c><00:00:03.040><c>1735</c><00:00:03.970><c></c><00:00:04.270><c></c><00:00:04.359><c></c><00:00:04.420><c>です</c><00:00:04.569><c>けれど</c><00:00:04.660><c></c><00:00:04.839><c>これ</c><00:00:05.109><c>ちょっと</c>

00:00:05.279 --> 00:00:05.289 align:start position:0%
でも一生声が未知のもので動く馬で全幅は1735右なんですけれどもこれちょっと

vtt形式の字幕がダウンロードできました。

字幕ファイルをtxtに変換する

https://pypi.org/project/webvtt-py/

Python上でvttからtxtにしてしまおう…… と考え始めてしまったものの、こうなるとバックエンド側(Python)の開発がメインになってしまうため、あくまで YouTube URL to vtt なサーバーとして構築します。vttを受け取ったクライアント側で処理すればよいわけです。ということでスキップ。

https://qiita.com/ShinsukeSutou/items/b12a6393515b560c0d15

Pythonインタプリタ上で使う

ターミナル上ではなく 〜.py 内で使う場合は以下のようにします。

youtube_dl.main(['--sub-lang', 'ja', '--write-auto-sub', '--skip-download', '--sub-format', 'vtt', '-o', '%(id)s', 'https://youtu.be/OagYLOhxf6A'])

この1行が実行後、同じディレクトリに OagYLOhxf6A.ja.vtt が生成されます。

Djangoで囲ってWebアプリ化する

この辺りを参考にしながらWeb上で動かせるようにミニマムに作っていきます。

https://qiita.com/sugurutakahashi12345/items/d4377a16c0e42cf48287

youtube-dlは十分に試せたので、新しくディレクトリを作り直してそこからDjangoで作成開始します。

1. プロジェクトとアプリを作成

今回は ytVttExporter という名前でプロジェクトを作成します。

# プロジェクトの作成
$ cd ~/Documents # 好きなところでよい
$ pip3 install django youtube-dl
$ django-admin startproject ytVttExporter
$ cd ytVttExporter

# ドメイン名/vttdl として使えるアプリを作成
$ python3 manage.py startapp vttdl

この時点で以下のような階層構造になっていればOKです。ちょっとややこしい。
Documentsはこちらの環境でそうなっているだけで、ご自身の環境でのルートがどこにあたるのかはしっかり確認しておいてください。

Documents/
 └ ytVttExporter/
    ├ vttdl/
    ├ ytVttExporter/
    └ manage.py

2. settings.pyへアプリを追加

~/Documents/ytVttExporter/ytVttExporter/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 追記
    'vttdl',
]

3. ルーティングを設定

プロジェクト側ではルーティングされるが、アプリ側ではそのまま実行、という形式になります。

https://ebi-works.com/django-routing/

~/Documents/ytVttExporter/ytVttExporter/urls.py
from django.contrib import admin
from django.urls import path
from vttdl import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('vttdl/', views.index),
]

4. 実際に実行されるページ(アプリ)を作る

ここが一番重要で、API本体となります。

コード(長いので折りたたみ)
~/Documents/ytVttExporter/vttdl/views.py
import youtube_dl
import os, threading # python組み込みなのでpipとかしなくていい
from django.http import HttpResponse

# ドメイン名/vttdl にアクセスがあったらこの関数が実行される
def index(request):

  # GETパラメータに「target」があるか確認する
  if 'target' in request.GET:

    # 今回はYouTubeの短縮URLまたは動画IDのいずれかが指定されていることを前提とする
    # 例: https://youtu.be/OagYLOhxf6A または OagYLOhxf6A
    if '/' in request.GET['target']:
      targetURL = request.GET['target']
      targetID = request.GET['target'].split('/')[-1]
    else:
      targetURL = 'https://youtu.be/' + request.GET['target']
      targetID = request.GET['target']
    resultVtt = targetID + '.ja.vtt'

    # youtube-dlは実行すると関数の最後で勝手にsys.exit()するので
    # 別関数を定義(末尾参照)して別スレッドで飛ばして実行し終了を待つ
    thread = threading.Thread(target=vttdl, args=([targetURL]))
    thread.start() # 開始
    thread.join() # 終わるまで待つ(これがないとJSの非同期みたいに先に進んでしまう)

    # ダウンロードされた字幕ファイルを開く(環境によってはエンコーディング注意しないといけないかも)
    vtt = open(resultVtt, 'r') # vttファイル開く
    body = vtt.read() # よみこむ
    res = HttpResponse(body) # HTTPレスポンスを作成する
    vtt.close() # ファイルを閉じる

    # 字幕ファイルを削除する(パーミッション問題が発生するかも)
    os.remove(resultVtt)

    # WebAPIとしてのレスポンス
    return res
  
  # パラメータがないときは何も返さない
  else:
    return HttpResponse('')


# youtube-dlで字幕ダウンロードする部分を別関数にしておく
def vttdl(targetURL):
  youtube_dl.main(['--sub-lang', 'ja', '--write-auto-sub', '--skip-download', '--sub-format', 'vtt', '-o' '%(id)s', targetURL])

5. マイグレーション

Django詳しくないので書いておいてよくわかってないですがやっといたほうがエラーでなくて心が安らぎます。

$ python3 manage.py migrate

6. 起動

$ python3 manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
June 24, 2021 - 11:12:37
Django version 3.2.4, using settings 'ytVttExporter.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Heroku上でAPIサーバーとして実行させる

下準備1

Heroku向けの依存関係を追加でインストールします。

pip3 install gunicorn psycopg2-binary django-heroku

manage.py があるのと同じディレクトリに、以下の3つのファイルを作成しておきます。

requirements.txt
youtube-dl==2021.6.6
Django
gunicorn
dj-database-url
psycopg2-binary
whitenoise
django-heroku
runtime.txt
python-3.9.5
Procfile
web: gunicorn ytVttExporter.wsgi

https://tutorial-extensions.djangogirls.org/ja/heroku

ところでgunicornとは何ぞやという場合は以下も参照。

https://devcenter.heroku.com/ja/articles/django-app-configuration

下準備2

ytVttExporter/ytVttExporter/settings.py をちょっとだけ再編集します。

先頭に追加
import django_heroku
末尾に追加
django_heroku.settings(locals())

ローカルで確認

$ heroku local web
10:16:54 PM web.1 |  [2021-06-24 22:16:54 +0900] [10611] [INFO] Starting gunicorn 20.1.0
10:16:54 PM web.1 |  [2021-06-24 22:16:54 +0900] [10611] [INFO] Listening at: http://0.0.0.0:5000 (10611)
10:16:54 PM web.1 |  [2021-06-24 22:16:54 +0900] [10611] [INFO] Using worker: sync
10:16:54 PM web.1 |  [2021-06-24 22:16:54 +0900] [10612] [INFO] Booting worker with pid: 10612

https://127.0.0.1/vttdl?target=[YouTubeのURL] にアクセスできることを確認しておきましょう。ここで落ちていればデプロイしてもダメです。

デプロイ

以下のスターターガイドに沿って進めていきます。

https://devcenter.heroku.com/ja/articles/getting-started-with-python?singlepage=true

Herokuへの登録とHeroku CLIのインストールは省略です。

# ディレクトリを確認しておく
$ pwd
/Users/*****/Documents/ytVttExporter

# ディレクトリ内のものを確認しておく
$ ls
db.sqlite3       manage.py        requirements.txt runtime.txt      vttdl            ytVttExporter

# ローカルリポジトリとして初期化
$ git init
Initialized empty Git repository in /Users/*****/Documents/ytVttExporter/.git/

# Herokuでログイン&作成
$ heroku create
Creating app... !
 ▸    Invalid credentials provided.
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/********
# ここでブラウザが開くのでログインボタンをクリック
Logging in... done
Logged in as ********************
Creating app... done, ⬢ sh******-s******-00000
https://sh******-s******-00000.herokuapp.com/ | https://git.heroku.com/sh******-s******-00000.git

# COLLECTSTATICを無効にしておく
$ heroku config:set DISABLE_COLLECTSTATIC=1
Setting DISABLE_COLLECTSTATIC and restarting ⬢ sh******-s******-00000... done, v1
DISABLE_COLLECTSTATIC: 1

# ファイル類をコミット
$ git add .
$ git commit -am "first"
[master (root-commit) bfe79c2] first
 26 files changed, 252 insertions(+)
 create mode 100644 db.sqlite3
 create mode 100755 manage.py
 create mode 100644 requirements.txt
 create mode 100644 runtime.txt
 ...
 .. # 省略

# Herokuにpushしてデプロイ
$ git push heroku master
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 12 threads
...
.. # 省略
remote: -----> Compressing...
remote:        Done: 64.8M
remote: -----> Launching...
remote:        Released v1
remote:        https://sh******-s******-00000.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/sh******-s******-00000.git
   bf2f551..957e66d  master -> master

# 起動できてるか確認
$ heroku logs --tail
2021-06-24T12:54:22.999033+00:00 app[api]: Deploy 0148d67d by user **********
2021-06-24T12:54:22.999033+00:00 app[api]: Release v1 created by user **********
2021-06-24T12:54:23.716721+00:00 heroku[web.1]: State changed from crashed to starting
2021-06-24T12:54:31.168905+00:00 heroku[web.1]: Starting process with command `gunicorn ytVttExporter.wsgi`
2021-06-24T12:54:33.900926+00:00 app[web.1]: [2021-06-24 12:54:33 +0000] [4] [INFO] Starting gunicorn 20.1.0
2021-06-24T12:54:33.901507+00:00 app[web.1]: [2021-06-24 12:54:33 +0000] [4] [INFO] Listening at: http://0.0.0.0:17072 (4)
2021-06-24T12:54:33.901686+00:00 app[web.1]: [2021-06-24 12:54:33 +0000] [4] [INFO] Using worker: sync
2021-06-24T12:54:33.905267+00:00 app[web.1]: [2021-06-24 12:54:33 +0000] [7] [INFO] Booting worker with pid: 7
2021-06-24T12:54:33.917033+00:00 app[web.1]: [2021-06-24 12:54:33 +0000] [8] [INFO] Booting worker with pid: 8
2021-06-24T12:54:35.000000+00:00 app[api]: Build succeeded
2021-06-24T12:54:35.386486+00:00 heroku[web.1]: State changed from starting to up

# この状態でアクセスしてみる
# https://sh******-s******-00000.herokuapp.com/vttdl?target=[YouTubeのURL]
2021-06-24T12:56:16.538766+00:00 heroku[router]: at=info method=GET path="/vttdl?target=https://youtu.be/qmyKungQIFA" host=sh******-s******-00000.herokuapp.com request_id=b946087a-e048-4b40-8cfc-817c4092beee fwd="***.**.4.32" dyno=web.1 connect=0ms service=2ms status=301 bytes=285 protocol=https
2021-06-24T12:56:16.538894+00:00 app[web.1]: 10.101.***.*** - - [24/Jun/2021:12:56:16 +0000] "GET /vttdl?target=https://youtu.be/qmyKungQIFA HTTP/1.1" 301 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
2021-06-24T12:56:17.469140+00:00 app[web.1]: [youtube] qmyKungQIFA: Downloading webpage
2021-06-24T12:56:18.188047+00:00 app[web.1]: [info] Writing video subtitles to: qmyKungQIFA.ja.vtt
2021-06-24T12:56:18.302440+00:00 app[web.1]: 10.101.***.*** - - [24/Jun/2021:12:56:18 +0000] "GET /vttdl/?target=https://youtu.be/qmyKungQIFA HTTP/1.1" 200 105269 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
2021-06-24T12:56:18.305122+00:00 heroku[router]: at=info method=GET path="/vttdl/?target=https://youtu.be/qmyKungQIFA" host=sh******-s******-00000.herokuapp.com request_id=b4405ae6-7d71-4d80-9156-27c164466de8 fwd="***.**.4.32" dyno=web.1 connect=1ms service=1591ms status=200 bytes=105512 protocol=https

Webアプリから使う場合はCORS対応を忘れずに

だいぶ長くなったので省略しましたが、これはAPIとしてNetlifyなどにあげたWebアプリからアクセスされることを想定しています。つまりCORSを有効にしておかないとaxiosなどからだと弾かれてしまう可能性があります。

https://qiita.com/karintou/items/52ee1f7c5fa641980188

上記を参考にして、

  • django-cors-headers のインストール(かつ requirement.txtへの追加)
  • settings.py の書き換え
    • 参考リンク内容に加え CORS_ORIGIN_ALLOW_ALL = True を追加しておくと、どのオリジンからも許可することができるのでおすすめ(ただし他人からもアクセスされるので慎重に)
    • 使用するWebアプリ側のオリジンが固定されているようなら CORS_ORIGIN_WHITELIST = [] を用いてオリジン指定しておくと安心

以上を実施すれば、任意のオリジンから配信されるWebアプリ内からもアクセスできるようになります。


おつかれさまでした!

Discussion