🐍

YoutubeDL(ytdlp)をAPI化した

2024/04/27に公開

最近は需要自体をあまり聞きませんが、昔は Youtube 動画をダウンロードしたいというニーズがありました。実は今でもあったりしますが、サブスクサービスに多くがとってかわられました。

しかし、お気に入りのクリエイターが突如作品をすべて消してしまう[1]、そもそもサブスクにならないなどの理由で、やっぱりダウンロードしておかないとだめだよね。という状況はかなりあります。

ので、yt-dlpというオープンソースの Web コンテンツダウンローダーを API 化してみました。
外部Webサービスのように、ダウンロードのたびにページを開く必要がなく、例えば次のようなiOSショートカットでボタンひとつで動画をダウンロードできるようになりました。

view

リポジトリについて

こちらになります。

https://github.com/nkte8/ytdlpServer/tree/main

HyperVなどでVMを立てることができたり、RaspberryPiにUbuntuを入れて使っている方は、セットアップをしてgit cloneしたdocker-compose.ymlで設定していただければと思います。

構築方法

端的に示すと、docker.iodocker-composecifsをインストールして、docker-compose up -dすれば動きます。

各種ソフトウェアをインストールして...

sudo apt update
sudo apt install docker.io cifs-utils
sudo gpasswd --add $USER docker
newgrp docker
sudo curl -SL https://github.com/docker/compose/releases/download/v2.4.1/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

設定を作成して

sudo mkdir -p /mnt/video
sudo tee "//<WindowsのIP>/<共有ディレクトリのパス>   /mnt/video   cifs  nofail,_netdev,x-systemd.automount,user=<ユーザ名>,password=<パスワード>,file_mode=0664,dir_mode=0775  0  0" /etc/fstab
sudo mount -a

サービスを起動するだけです。

docker-compose up -d

これでもうサーバの5000番に対してHTTPリクエストを投げれば動画をダウンロードできます。

詳しいインストール方法はREADME.md に詳しく書いていますが、単に使うだけなら上で十分です。

動機について

導入に書いた通りで、もう一つの目的として、以前、ラズパイ ×KEDA でサーバーレス入門という名目で作成したものを、より手軽に展開したいという部分でリファクターを行った形になります。

こちらが前プロジェクトです。今見るとかなり滑稽な景色が広がっておりますが...

https://github.com/nkte8/serverless/tree/main

仕組み

3種類のコンテナをDocker-composeにて構築しています。
また、保存先としてWindowsでのファイル共有で共有されたパスをUbuntuへマウントし、これに動画を保存するようにしました。

  • ytdlpserver-api
    • ユーザの http リクエストを受ける口
  • redis
    • リクエストを一時的に格納する DB
  • ytdlpserver-worker
    • ytdlp を実際に実行してファイルを保存する機構
  • Windows マシン
    • Windows の共有機能(Samba/Cifs と呼ばれるもの)で共有

server

技術仕様

  • ytdlp はPython制のプログラムなので、プログラム自体もPythonで書いた
  • Python で API を展開するにあたって、一番使われているであろうFlaskライブラリを採用した
    • リリース展開もwaitressserveに置き換えるだけでよいため、非常に簡単
  • 可能な限り簡単にサービスを展開したかったため、コンテナ技術を用いた
    • 今回はDocker(Docker-compose)を使うことにした
    • コンテナは可能な限り容量を減らしたかったため、Alpine Linuxを用いた
  • 非同期で処理をしたかったため、メッセージキューを実装する必要があった
    • 元のプロジェクトではイベントドリブン(イベントが発火された際にのみリソースを確保する仕組み)を用いていた
    • Docker-composeを使い、元プロジェクトではCNIを使っていた内容をbridgeに簡略化した
  • Python で非同期処理を簡単に実装できるrqライブラリ(Redis Queue)を用いた

インフラについて

構築について記載していきます。

構築するUbuntu環境

今回はWindows上にHyperVで仮想サーバを構築しました。

hyperv

インターフェースは外部ネットワークに直に接続していて(NATではなく、仮想サーバ自体がホストマシンのNICを専有している状態)ベアメタル的な使い方をしています。[2]

ネットワークは以下のような感じです。Outboud Switchという仮想スイッチを定義していて、これをマシンに設定しています。

network

これはRealtek PCIe GbE(イーサネットアダプタ)に直接紐づいています。NICとスイッチが一対一で紐づいているため、仮想マシンにIPアドレスを割り当てることができます。

プライベートネットワークであればどんなデバイスからでもアクセスが可能で、iPhoneやiPadからリクエストを投げることができるのも、この設定のおかげです。

docker-compose.yml

docker compose は以下のようになっています。

docker-compose.yml
version: '3'

services:
  redis:
    image: redis

  api:
    build:
      dockerfile: ./Dockerfile.api
    depends_on:
      - redis
    environment:
      RQ_REDIS_URL: redis://redis
    ports:
      - 5000:5000

  worker:
    build:
      dockerfile: ./Dockerfile.worker
    depends_on:
      - redis
    environment:
      RQ_REDIS_URL: redis://redis
    volumes:
      - /mnt/video:/download
    working_dir: /download
  • 外部からの情報を受けるのはservice: apiのみで、その他のコンテナについてはコンテナネットワーク(docker-compose の機能)でつながっているため、意識する必要がありません。
    • ポートは5000番を開けています。左辺に利用したいポートを設定してください。
        api:
          build:
        ...(省略)...
          ports:
            - 5000:5000
      
  • service: workerではダウンロードした動画を保存する先をマウントしています。この辺のコードです
    volumes:
      - /mnt/video:/download
    working_dir: /download
    
    コードではワークディレクトリにダウンロードをするため、Docker-composeでホストのディレクトリをバインドし、狙った箇所にダウンロードするようにしています。
  • Redis へのアクセスはredis://redisで行います。本プログラムを書くために、参考した親記事がそうしていた[3]ためRQ_REDIS_URLへ環境変数としていますが、埋め込みでも問題ないです。

プログラムについて

Pythonプログラムについて、軽く書いておきます。

動的型付け言語について

一般のプログラム、例えばTypescriptは静的型付け言語といい、基本的にはきちっとルールを決めて進めるタイプの言語です。

let a: number = 100;
a = a.toString() + "is 100"; // <--- 型が違うため、エディタ上でエラーしてくれる
a += 1;

型は常に定義に従っていることを強制されるため、コードを読む場合はこれをなぞるだけで問題ありません。

一方、Python は動的型付け言語といって、型情報が割と適当に設定できてしまう言語です。

a = 100 ## -> 勝手にintとして判断され、intになる
a = str(a) + "is 100" ## -> aはintからstrに変換されるため、エラーしない
a += 1 ## -> エディタ上ではエラーしないが、上の工程でaはstr型になっているため、エラーする

もちろんエラーとして表示されないだけで、実際の処理で矛盾が発生するなどした場合は処理が停止します

このため、変数の状態は常に監視しておく必要がありますきも...
動的型付け言語であることを生かして、型によって処理を分岐などができれば Python マスターなのかもしれませんが、非常に難しい領域だと思います。

コーディングルールの設定

上記のように、Python は非常に難しい言語であるため、プロジェクトではruffという Linter/Formatter を VSCode プラグインとして導入しました。

https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff

拡張をいれると Formatter が使えるようになるため、Shift+Alt+Fでコードを整頓してくれたり、読みやすいコードになるようにエディタが注意をしてくれます。

本プロジェクトでは、基本的にはすべてのルールに従い、便宜上不要そうな箇所についてのみignoreで無視する設定をいれるような形で記載しました。

[tool.ruff]
select = ["ALL"]
ignore = [
    "D",
    "T201",
    "TRY301",
    "BLE001",
    "S104",
]

実際のコードについて

ひねったことはしていないので、あくまで概要レベルで記載しておきます

  • main.py
    • APIの受け口になるモジュール。FlaskでHTTPメソッドを受け付けています。
  • function.py
    • ytdlpで処理する内容を記載しているモジュール。rqが呼び出すために使います。
  • worker.py
    • rqがredisと接続し、処理を実行するために記載されます。
  • __init__.py
    • モジュールの最初に読み込まれますが、特に何もしていません。

今後の課題

  • セットアップ手順の前段として、Ubuntu Serverを実行する方法が記載されているべき
    • そもそもUbuntu Server環境を持っている利用者しか利用ができない状況
      • とはいえ外部サービスとして展開するのは、本プロダクトの趣旨から外れているため、あくまで環境構築までパッケージ化されている必要がある
    • DockerDesktopでの利用方法も図式化するべきか?
  • DockerではなくPodmanを使うべき[4]
    • Dockerの雲行きが怪しいため
    • ただし、手軽さを重視するべきであるため必須ではない。
  • RaspberryPiで気軽に使えるようなパッケージにしてもいいかもしれない
    • これはもともとのプロジェクト(https://github.com/nkte8/serverless )で実現したかった内容
    • kubernetesよりはライトに実装できれば実用的であるが、現状は難しい。

所感

語るほどの内容でもありませんが、そこそこ便利なものが用意できたのかなと思います。
少なくとも、自身でパソコンを壊してしまった際には立て直しができそうです。

本記事は、せっかく対外的にも使えるものを作ったにも関わらず、物珍しいわけでもなく、おそらく埋もれてしまうと考え、自身の整理も兼ねて記事執筆をしてみました。

お読みの方にとって、なにかの足しになれば幸いです。

脚注
  1. ハヤシダ(林田匠)さんのひみつのうた(KarenT)が AppleMusic から消えたときは本当に焦りました。楽曲購入派で本当に良かった。AppleMusic 上から消えていることで、アートワークの再取得ができなかった起因で発覚しました。 ↩︎

  2. この構成、いろんなデバイスからアクセス可能な開発環境としてかなり優秀なので、おすすめです。VSCodeを使っている場合はRemote Developmentなんかを使うと、ほぼネイティブに使うことができます。 ↩︎

  3. Python で分散タスクキュー (RQ 編)の内容そのままです。なんの工夫もせずキューイングができるの、めちゃくちゃすごい。 ↩︎

  4. 一応いろいろ試しはしました。コメント部分に形跡を残しています。CNI あたりがなかなか移行の障壁になりそうです。 ↩︎

GitHubで編集を提案

Discussion