YouTube動画をダウンロードするWebアプリを作る

# 始めに
YouTube動画をダウンロードするにあたって、無料のWebサービスはありますが、怪しい広告が表示されたりして危なかったり、煩わしかったりします。
ライブラリを使えば自作も簡単にできますので、実装内容についてまとめました。
ここでは以下のものを使って実装しています。

+ Node.js
+ TypeScript
+ ytdl-core
+ Express
+ Heroku

# ローカルでYouTube動画をダウンロードする
まず始めにローカルでYouTube動画をダウンロードできるようにします。

以下の記事を参考にして、スクリプトを書きました。ただしこちらではTypeScriptを使って実装しています（と言っても型定義全くないですけど。。）。

[Node.jsでYoutube動画をDLして保存するytdl-coreを使ったみたメモ](https://qiita.com/n0bisuke/items/af20c95a3dc6c22e5792)

```typescript:download.ts
import ytdl from 'ytdl-core';
import path from 'path';
import fs from 'fs';

const BASE_URL = 'https://www.youtube.com/watch?v=';

const YOUTUBE_ID = 'bnc1NjaXXXX';

const url = `${BASE_URL}${YOUTUBE_ID}`;

const video = ytdl(url);

video.pipe(fs.createWriteStream(path.resolve(__dirname, `./tmp/${YOUTUBE_ID}.mp4`)));

video.on('end', () => {
  console.log('file downloaded.');
});
```

これでmp4ファイルがダウンロードされました。mp3はffmpegで変換する必要があるようなのでchild_processで変換します。

```typescript:mp3に変換
import path from 'path';
import { exec } from 'child_process';

video.on('end', () => {
  const inputFilePath = path.resolve(__dirname, `./tmp/${YOUTUBE_ID}.mp4`);
  const outputFilePath = path.resolve(__dirname, `./tmp/${YOUTUBE_ID}.mp3`);
  // ffmpegでmp3に変換する。yオプションで上書きができる（これがないと、出力先にファイルが存在している場合は止まってしまう）
  exec(`ffmpeg -y -i ${inputFilePath} ${outputFilePath}`, (error, stdout, stderr) => {
    if (error) {
      console.error(error);
      return;
    }
    console.log(stdout);
    console.log(stderr);
  });
});
```

# ExpressでYouTube動画をダウンロードするAPIを作る
ローカルでダウンロードできるようになったので、次はExpressでサーバーを立てて、API経由でダウンロードして動画または音声を返すAPIを作ります。
`/api/youtube/bnc1NjaXXXX?fileType=mp4`みたいに送ったら対象のYouTubeIdをmp4またはmp3でダウンロードするようにします。

```typescript:server.ts
// Youtubeのダウンロード
app.get('/api/youtube/:youtubeId', (req, res) => {
  const { youtubeId } = req.params;
  const fileType = (req.query.fileType || 'mp4') as 'mp4' | 'mp3';

  const destFilePath = path.resolve(__dirname, `./tmp/${youtubeId}.mp4`);

  const url = `https://www.youtube.com/watch?v=${youtubeId}`;
  const stream = ytdl(url, { quality: 'highest' });

  stream.pipe(fs.createWriteStream(destFilePath));

  stream.on('error', (err) => {
    console.error(err);
    res.status(400).send('download error!');
  });

  stream.on('end', () => {
    console.log(`youtube file (${youtubeId}.mp4) downloaded.`);

    // mp4の場合はそのまま返す
    if (fileType === 'mp4') {
      res.download(destFilePath);
      return;
    }

    // mp3の場合は変換してから返す
    console.log('transform mp4 -> mp3.');
    const mp3FilePath = path.resolve(__dirname, `./tmp/${youtubeId}.mp3`);
    exec(`ffmpeg -y -i ${destFilePath} ${mp3FilePath}`, (err, stdout, stderr) => {
      if (err) {
        console.error(err);
        res.status(500).send('movie translation error!');
        return;
      }
      console.log(stdout);
      console.log(stderr);
      res.download(mp3FilePath);
    });
  });
});
```

フロント側のリクエストは以下のようにしました。ファイルのダウンロードは直接URL遷移してもできますが、エラーの場合だとエラーページが出てしまうので、一度blobで受け取るようにしました。（どうでも良いですが、fetch APIのエラーハンドリングは一々`response.ok`をチェックしないといけないのは面倒ですね。。）

```javascript
console.log('downloading...');
// ファイルダウンロード
fetch(`/api/youtube/${youtubeId}?fileType=${fileType}`)
  .then((response) => {
    if (!response.ok) {
      throw new Error('Network error');
    }
    return response.blob();
  })
  .then((blob) => {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${youtubeId}.${fileType}`;
    document.body.appendChild(a);
    a.click();
    a.remove();
  })
  .finally(() => {
    console.log('finished.');
  });
});
```

# Herokuにデプロイする
最後にHerokuにデプロイします。ExpressをHerokuにデプロイするやり方とほぼ同じですが、今回はffmpegを入れる必要があります。こちらに書かれていたbuildpackを使用しましたが、heroku-cliを使うのが面倒だったのでGUIで入れました。

[Herokuでffmpegを使えるようにする](https://qiita.com/akabei/items/8fd0dc0c2cd2c9cad0c0)

Herokuのアプリ画面に入り、SettingsのBuildpacksというセクションでbuildpackを２つ入れます。今までは自動でheroku/nodejsが使われていたと思いますが、２つ入れる必要があるので明記する必要があります。

+ heroku/nodejs
+ https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest

![スクリーンショット 2021-04-04 15.17.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187768/7ea1a519-8a35-f538-2106-522c8e1ca017.png)

これで無事デプロイされて動作すれば完了です。

# 終わりに
以上がYouTube動画をダウンロードするWebアプリを作る流れでした。今回Node.jsでYouTube動画をダウンロードしましたが、音声付きだと画質は360pしか選べなくて少し残念でした。昔PythonでダウンロードしたときはフルHDでも普通にダウンロードできた気がしたんですけどね。。

今回作成したソースとHerokuアプリは以下に上がっております。興味がある方は見てください😄

+ [リポジトリ](https://github.com/wintyo/youtube-downloader)
+ [Herokuアプリ](https://youtube-downloader-wintyo.herokuapp.com/)