Fast API x Reactで動画保存システムの構築

# 概要
こちらの記事はReact x Fast APIで動画保存システムを作ってみたという内容になります
これまで記事を読んでくださった方は分かるかもしれませんが、今まで学んだことを形にしてみようという感じです

# バージョン情報
yt-dlp==2024.12.6
fastapi==0.115.6
uvicorn==0.32.1

"axios": "^1.7.9",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"typescript": "~5.7.2",

# イメージ図
構成は至ってシンプルな形です
React,TypeScriptで画面を作り、裏側のFast APIで作ったエンドポイントを呼び出すといった流れをとります

![名称未設定ファイル.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701238/53eaa30d-d54e-4de4-b282-799dba3a5ebe.png)

(本当はバックエンドからファイルを返す形にしようとしていましたが、ちょっと間に合わず、、)

# 実装内容

## フロント
cssなどもありますが、一旦実際のロジック部分を記載します
```ts
import React, { useState } from 'react';
import axios from 'axios';
import './App.css';  // CSSをインポート

const VideoDownloader = () => {
  const [targetLink, setUrl] = useState('');
  const [message, setMessage] = useState('');

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    try {
      // FastAPIエンドポイントにPOSTリクエストを送信
      const response = await axios.post(
        'http://localhost:xxxx/yt_download',
        { url: targetLink },
        {
          headers: {
            'Content-Type': 'application/json',
          },
        },
      );

      if (response.status === 200) {
        setMessage('動画のダウンロードが完了しました!');
      } else {
        setMessage('動画のダウンロードが完了していません、再度お試しください。');
      }

    } catch (error: any) {
      // エラーハンドリング
      setMessage(`エラー: ${error.response.status} - ${error.response.data.detail}`);
    }
  };

  return (
    <div className="container">
      <h1>動画URLを入力してください</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={targetLink}
          onChange={(e) => setUrl(e.target.value)}
          placeholder="動画のURL"
        />
        <button type="submit">ダウンロード</button>
      </form>
      {message && <p>{message}</p>}
    </div>
  );
};

export default VideoDownloader;
```

- `export default VideoDownloader;`でコンポーネントとして定義し、他ファイルでもインポートできるような形にしております
- axiosというものを使用して、バックエンドに向けてリクエストを送っています
- returnの部分でダウンロード前後の画面表示を行っています
    - ダウンロードの処理が成功失敗によって、メッセージを渡してreturn側で処理するといった感じです

## バックエンド
こちらは[以前紹介した記事](https://qiita.com/ryo0819/items/adfde9b905cbdffb2be1)と大きくは変わっていません
ただ、2点だけ変更を加えております

1点目がCORSエラー対策です

これは「同一オリジンポリシー(SOP)の制限を回避する際に利用する機能のこと。異なるオリジンからのアクセスを許可できる仕組み」のようです
正直いってまだまだ理解が浅いのでなんとも言えません

いろいろ情報をみて理解した範囲ではhttp://localhost同士で通信するとエラーが出るぞといった感覚です
そのため、バックエンドではCORSエラーを回避する設定が必要になります

Fast APIを使っている場合はfastapi.middleware.corsからCORSMiddlewareを使用してどういうoriginを許可するかどのリクエストメソッドを許可するかなどを設定することができます

以下は使用例でhttp://localhost:xxxxを許可する、リクエストメソッドはなんでも良いといった感じですね

```py
from fastapi.middleware.cors import CORSMiddleware

origins = ["http://localhost:xxxx"]
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
```

2点目が動画ダウンロードのメソッド内でURLでないものが渡された場合に、400エラーをとメッセージを返すように変更しております
(if re.matchの部分はwithよりも外でも良いような気がします)

```py
@app.post("/yt_download")
def download_yt_video(jsonbody: get_yt):
    try:
        download_link = jsonbody.url
        with YoutubeDL(ytdlp_opts) as ydl:
            if re.match(URL_PATTERN, download_link):
                ydl.download(download_link)
            else:
                raise HTTPException(status_code=400)
    except HTTPException:
        raise HTTPException(
            detail="不正なURLです。URLを入力してください。"
        )
    except Exception:
        raise HTTPException(status_code=500, detail="予期せぬエラーが発生しました。")
```

# 完成した画面
- 初期画面
![スクリーンショット 2025-03-01 23.31.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701238/5096283e-0bf4-4c74-85ad-6e191381b5f1.png)

- ダウンロード成功
![スクリーンショット 2025-03-01 23.42.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701238/d679b08c-0bc8-4bae-b0cf-1c0246e14a54.png)

- ダウンロード失敗
![スクリーンショット 2025-03-01 23.55.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701238/7293f7d7-274c-40fe-954d-edfadf1cffb9.png)

# おわりに
ということで、フロントとバックエンドを作ってみたという回でした

今後もまた何か作ってみたいと思います！