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