実は複雑なYouTubeチャンネルURLの種類とバリデーション

YouTube関連のコンテンツをまとめるとき、YouTubeチャンネルURLのバリデーションをしたいことがあります。
しかし、意外と大変である割に、ググっても情報が出てこなかったので、独自に調べた情報をまとめておこうと思います。

## URLの種類

YouTubeチャンネルには、４パターンのURLが存在します。

| 名前                      | 概要                                                         | 例                                                           |
| ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| チャンネルURL（IDに基づくURL）| 全てのチャンネルに割り当てられているチャンネルIDを含むURL。 | `youtube.com/channel/{{チャンネルID}}` |
| 以前のユーザ名のURL| チャンネルを作成するときにユーザ名の指定が必要だったころの名残。 | `youtube.com/user/{{ユーザID}}`  |
| カスタムURL     | URLを覚えやすくしたり、短縮する目的で使用される。 ユーザがYouTubeに申請することで利用できるようになる。  | `youtube.com/c/{{カスタムチャンネルID}}` |
| 短縮バージョンのカスタムURL |以前のユーザ名のURL、もしくはカスタムURLが存在するときに自動的に割り振られる。**ただし、両方存在した場合には、別のチャンネルであってもカスタムURLが優先される。**| `youtube.com/{{カスタムチャンネルID}}` |

### 補足
> 
* カスタムURLと以前のユーザ名のURLは独立しています。一つのチャンネルに対して両方が作成されている場合、片方だけが作成されている場合、両方作成されていない場合があります。
* 作成されていないURLに対してアクセスした場合、404が返ります。
* カスタムユーザURLとカスタムチャンネルURLのIDには同じIDを指定することもできます。
* 現在、以前のユーザ名のURLを新規作成することはできません。
* 既に存在する以前のユーザ名のURLに含まれるIDを、カスタムURLとして申請できます。その場合、短縮バージョンのカスタムURLのルーティングは、以前のユーザ名のURLからカスタムURLに上書きされます。

## URL判別フローチャート

![youtube.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227791/b35ce425-bb04-efcb-0e5c-bb40a07da358.png)

## JavaScriptコードサンプル

### 有効なURLか判別
URLを受け取り初期化するクラス。チャンネルURLの種類は問わない。
（若干TypeScriptの記法が混ざっています）

```typescript
export class YoutubeChannelUrl {
  private _url: string;

  // 全部じゃないけど、www.youtube.com/にある予約されたパス
  private _rootReservedWords = [
    'watch',
    'feed',
    'playlist',
    'user',
    'c',
    'channel',
    'paid_memberships'
  ];

  constructor (rawUrl: string) {
    const url = encodeURI(rawUrl)

    // チャンネルURL
    const patternChannel = /youtube.com\/channel\/UC[\w-]{22}/
    const patternChannelMatched = url.match(patternChannel)?.[0]
    // 以前のユーザ名のURL
    const patternUser = /youtube.com\/user\/[\w-%]+/
    const patternUserMatched = url.match(patternUser)?.[0]
    // カスタムURL
    const patternCustom = /youtube.com\/c\/[\w-%]+/
    const patternCustomMatched = url.match(patternCustom)?.[0]
    // 短縮バージョンのカスタムURL
    const patternShort = /youtube.com\/([\w-%]+)/
    const patternShortMatched = url.match(patternShort)

    if (patternChannelMatched) {
      this._url = patternChannelMatched
    } else if (patternUserMatched) {
      this._url = patternUserMatched
    } else if (patternCustomMatched) {
      this._url = patternCustomMatched
    } else if (patternShortMatched) {
      console.log(patternShortMatched)
      if (this._rootReservedWords.includes(patternShortMatched[1])) {
        throw new Error(
          `指定されたYouTubeチャンネルURLは不正なフォーマットです。（${url}）`
        )
      }
      this._url = patternShortMatched[0]
    } else {
      throw new Error(
        `指定されたYouTubeチャンネルURLは不正なフォーマットです。（${url}）`
      )
    }
  }

  toString (): string {
    return this._url
  }
}

const url1 = new YoutubeChannelUrl('https://www.youtube.com/channel/UC9ohoh_uz___-ulOac6kk8w')
const url2 = new YoutubeChannelUrl('https://www.youtube.com/user/hoge')
const url3 = new YoutubeChannelUrl('https://www.youtube.com/c/hoge')
const url4 = new YoutubeChannelUrl('https://www.youtube.com/hoge')
```

## 注意点
コードの`_rootReservedWords`を見ればわかる通り、「短縮バージョンのカスタムURL」がチャンネルURLか判別するためにはYouTubeが使用しているページURLを把握する必要があるので、完全に漏れを許さないシステムでは「短縮バージョンのカスタムURL」をバリデーションするべきではないと思います。

また、私の調べた限りだと、「カスタムURL」から「チャンネルURL（IDに基づくURL）」をAPI経由で取得する方法は存在しません。（YouTube Channel APIにおいて、カスタムURLでチャンネルを特定することができません。）
「以前のユーザ名のURL」であれば、`forUsername`パラメータを用いてチャンネルを特定することができます。

手動で「カスタムURL」から「チャンネルURL（IDに基づくURL）」を取得する方法としては、一旦ブラウザで「カスタムURL」にアクセスし、そのチャンネルの動画ページに移動し、そのページ内のチャンネルアイコンをクリックすることで、「チャンネルURL（IDに基づくURL）」を取得できます。（最初と同じページだが、URLが変わっている）

## 参考
* [YouTube ヘルプ > カスタム URL の概要](https://support.google.com/youtube/answer/2657968?hl=ja)
* [YouTube Data API > Channels](https://developers.google.com/youtube/v3/docs/channels)

<!--
@startuml
hide circle
scale 4000 width
skinparam linetype ortho\
skinparam classAttributeIconSize 0
' skinparam monochrome true

(*) -- youtube.com/channel/UC{64進数固定長22}

if "" then
  -- [Yes] デフォルトURL #DDD
else
  -- [No] "youtube.com/user/{文字列}\nに当てはまるか"

  if "" then
    -- [Yes] 以前のユーザ名のURL #DDD
  else
    -- [No] "youtube.com/c/{文字列}\nに当てはまるか"

    if "" then
      -- [Yes] カスタムURL #EEE
    else
      -- [No] "youtube.com/{文字列}\nに当てはまるか"

      if "" then
        -- [Yes] "短縮バージョンのカスタムURL" #DDD
        if "" then
          -- [以前のユーザ名のURL\nにのみ書き換えられる場合] 以前のユーザ名のURL
        else
          -- [カスタムURLにのみ\n書き換えられる場合] カスタムURL
        else
          -- [以前のユーザ名のURLと\nカスタムURLの両方に\n書き換えられる場合] 以前のユーザ名のURLとカスタムURLが両方存在する
        endif
      else
        -- [No] YoutubeチャンネルのURLではない
      endif
    endif
  endif
endif

@enduml
-->