LoginSignup
6
1

More than 3 years have passed since last update.

この記事は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

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

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

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

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

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

Dockerfile

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で色々するのも選択肢になるんじゃないのかなと思います。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1