Next.jsを変な使い方してみた

この記事はNext.js Advent Calendar 2019 15日目の記事です。

## はじめに

YoutubeLiveを録画する自分用のアプリをさくっと作りたかったのでまず技術選定をしました

| 候補         |  |
| ----------- | --------------------------------- |
| CLI（Node.js）         | GUIがない、スマホから操作できない |
| BFF + React | めんどい                          |
| Next.js     | Node.js + GUI をさくっと作れる？  |

今回はNext.jsを選んでみました

完成品のソースコードは↓にあります
https://github.com/shinyoshiaki/youtube-downloader

## Next.jsをサーバサイドElectron？と見立てて使う

Next.jsはgetInitialProps内がNode.jsのコンテキストで動いてElectron的な使い方ができそうだったので、getInitialProps内で```ytdl-core```というNode.js用のYoutubeの動画を録画するライブラリを使ってみました。

さっそくgetInitialProps内で```ytdl-core```を使っている箇所を見ていきましょう

pages/dl.tsx

```typescript
import { NextPage } from "next";
import { Button } from "@material-ui/core";
import Link from "next/link";
import { download, Progress, finishDownload } from "../src/domain/youtube";

type Props = {
  status: Progress;
  id?: string;
};

const DlPage: NextPage<Props> = ({ id, status }) => {
  const renderStatus = () => {
    switch (status) {
      case "completed":
        return (
          <a href={`/static/${id}.mp4`} download>
            Click to download
          </a>
        );
      case "downloading":
        return (
          <div>
            <p>downloading</p>
            <a href={`/dl?id=${id}&finish=true`}>
              <Button>finish</Button>
            </a>
          </div>
        );
      case "fail":
        return <p>fail</p>;
    }
  };

  return (
    <div>
      <Link href={`/`}>
        <Button>Home</Button>
      </Link>
      {renderStatus()}
    </div>
  );
};

DlPage.getInitialProps = async ({ query }) => {
  let { id, finish } = query as any;

  if (finish && id) {
    await finishDownload(id);
    return { status: "completed", id };
  }

  if (id) {
    try {
      const status = await download(id);
      return { status, id };
    } catch (error) {
      return { status: "fail" };
    }
  } else return { status: "fail" };
};

export default DlPage;

```

getInitialPropsでdownloadという関数を実行しています。この関数の中でytdl-coreを使っています。download関数の中を見てみましょう

src/domain/youtube.ts

```typescript
import fs from "fs";
const ytdl = require("ytdl-core");

export const download = async (youtubeId: string): Promise<Progress> => {
  const BASE_PATH = `https://www.youtube.com/watch?v=`;

  const url = BASE_PATH + youtubeId;

  const error = await new Promise<object | undefined>(r =>
    ytdl.getInfo(url, (err: object) => {
      if (!err) {
        r(undefined);
      } else r(err);
    })
  );

  if (error) {
    console.warn("error", error);
    return "fail";
  }

  return await new Promise<Progress>(async r => {
    try {
      const lock = `${youtubeId}.dl`;
      const file = `${youtubeId}.mp4`;

      if (!fs.existsSync(lock)) {
        if (fs.existsSync(`static/${file}`)) {
          if (fs.existsSync(file)) fs.unlinkSync(file);
          r("completed");
          return;
        }

        ytdl(url)
          .on("response", () => {
            fs.rename(file, `static/${file}`, () => {
              fs.unlinkSync(lock);
              r("completed");
            });
          })
          .on("data", (data: Buffer) => {
            console.log("ondata", data.length);
            fs.writeFileSync(lock, "");
          })
          .pipe(fs.createWriteStream(file));

        setTimeout(() => {
          r("downloading");
        }, 3000);
      } else {
        r("downloading");
      }
    } catch (error) {
      console.log({ error });
      r("fail");
    }
  });
};
```

がっつり***fs***をインポートしているのでこのコードは少なくともブラウザでは動作しませんし、そもそもそのままだとビルドすら通らないので、next.config.jsを少し書き換えます。

***next.config.js***

```javascript
module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.node = {
        fs: "empty"
      };
    }
    return config;
  },
  routes: [{ src: "^/static/(.*)", dest: "/static/$1" }]
};

```

こうしてやるとビルドできます。

最後にDockerfileにまとめてサーバーにぶちこめば、スマホから操作をして、サーバー側で録画保存できるウェブサービスの完成です！

Dockerfile

```yaml
FROM node:11.15.0-stretch AS build

RUN apt update &&\
    apt install git curl && \
    curl -o- -L https://yarnpkg.com/install.sh | sh

ENV PATH $HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH

RUN mkdir /next
WORKDIR /next
COPY . .
RUN yarn

EXPOSE 3000

CMD [ "yarn","serve" ]
```



## あとがき

Next.jsも本来は、getInitialPropsで色々するんじゃなくてサーバーを建ててそっち側で色々してBFF風にするのが正しいのですが、一応今回のように自分用のウェブサービスをさくっと作りたい場合はgetInitialPropsで色々するのも選択肢になるんじゃないのかなと思います。