20210127のNode.jsに関する記事は13件です。

【docker】Node.jsのコンテナ立ち上げてサンプルアプリを動かしたい

この記事の目標

dockerのコンテナ立ち上げて、このアプリ↓をローカル環境で立ち上げる

https://github.com/justadudewhohacks/face-api.js

GitHubからリポジトリcloneする

ローカルの任意のディレクトリにcloneする

$ git clone https://github.com/justadudewhohacks/face-api.js

docker hubからnode.jsのイメージpullしてくる

alpineが軽量でオススメらしいので、v14のをpullする

$ docker pull node:14-alpine

イメージをpullできたことの確認

$ docker images

コンテナ立ち上げる

docker run -it -v $PWD:/workspace -p 8100:3000 --name node-face-api node:14-alpine /bin/ash
  • -itで、コンテナ内で入力可能な状態にする
  • -v $PWD:/workspaceで、カレントディレクトリのファイルをコンテナ内の「workspace」ディレクトリにマウントするように指定
    • そのため、リポジトリをcloneしたディレクトリで作業する必要あり
    • $PWD$(pwd)といった指定方法もありらしい
  • -p 8100:3000で、ホスト側のポート8100とコンテナ側のポート3000を繋ぐ
    • このホスト側のポート番号は空いているものを適宜使う
    • コンテナ側はよしなに設定
    • ポート使用状況はMacの場合「ネットワークユーティリティ」で確認できる
  • --name node-face-apiで、コンテナ名を「node-face-api」に設定
  • node:14-alpineで、イメージとバージョン(TAG?)の指定
  • /bin/ashで、コンテナ内で使うコマンドを指定

今回は使ってないが、以下のオプションもよく使われる

  • --rmで、コンテナから出た時にコンテナ削除

※ちなみに、コマンドの記述順は、以下の通りらしい

$ docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

起動時にエラーが起こって上手くコンテナに入れなかった場合、以下の手順でコンテナ削除し、
runコマンド見直してから再度試す

# コンテナ一覧表示
$ docker ps -a

# 起動(作成)に失敗したコンテナのIDを指定してコンテナ削除
$ docker rm [CONTAINER ID]

コンテナ内でアプリケーションを立ち上げる

コンテナ作成して中に入ると、ホストのファイル群がworkspaceディレクトリ内にあるはずなので、確認する

$ cd workspace
$ ls

あとは、face-ap.jsのチュートリアル通りに進めていくだけ

$ cd examples/examples-browser
$ npm install
$ npm start

ホストマシンのポートは8100に指定したので、以下にアクセス

http://localhost:8100/

それっぽいページが表示されればOK!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pug内のパスをnodeの開発環境で切り替える

テスト環境/本番環境でディレクトリ階層が違というプロジェクトがあり、pugの変数をビルドのたびに手動で切り替えるようにしていました。
「nodeでビルドするんだからnodeの環境開発で切り替えれるっしょ!!」
と思ってやってみたらやっぱり出来たので、備忘録として書き残しておきます。

環境

  • pug ^3.0.0
  • html-webpack-plugin ^4.5.1
  • pug-loader ^2.4.0

pugは、pug-loaderhtml-webpack-pluginを用いてwebpack内でビルドしています。
ここでのミッションは、webpack経由でどうやってpugに環境変数を埋め込むか?です。
(下記の方法はpug出なくても素のhtmlでも可能です。)

pug内に環境変数を埋め込む

  plugins: [
    new HtmlWebpackPlugin({
      title: 'My App',
      filename: 'assets/admin.html'
    })
  ]

これを

  plugins: [
    new HtmlWebpackPlugin({
      title: 'My App',
      filename: 'assets/admin.html',
      environment: process.env.NODE_ENV
    })
  ]

こうですね。environmentはドキュメントなどにある提供されているプロパティではなく勝手につけたプロパティのようです。
つまり foo でも test でもなんでもOKです。

- var appEnv = htmlWebpackPlugin.options.environment;

pug内で上記のよう先ほど追加したプロパティ経由で process.env.NODE_ENV にアクセスすれば、dev/proでpugを分岐出来ます。

もちろん、コマンドを叩くときはNODE_ENVのセットを忘れずに。

package.json
"dev": "NODE_ENV=development webpack --mode production",
"build": "NODE_ENV=production webpack --mode production"

追記

ちなみに modeを指定した場合、コマンドで指定した NODE_ENVは無視されて
mode production ではprocess.env.NODE_ENVにproductionが、
mode developmentではprocess.env.NODE_ENVにproductionが入ってしまいます。
これをやめたい場合、webpackにoptimizationのnodeEnvをfalseにし、
DefinePlugin経由でNODE_ENVを渡してあげるといいです。

webpack
optimization: {
 nodeEnv: false,
},
webpack
plugins: [
  new webpack.DefinePlugin({
     NODE_ENV: JSON.stringify(process.env.NODE_ENV),
  }),
],
js
console.log(`node_env: ${NODE_ENV}`) // NODE_ENV=test とコマンドで叩くと testが取得できる

おまけ HTMLでは?

<body class="<%= htmlWebpackPlugin.options.environment %>">

参考サイト

https://stackoverflow.com/questions/39902197/how-can-i-pass-webpack-environment-variables-in-html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

M1マシンでflowが起動しない問題の対応

事象

M1(apple silicon)マシンでrosettaを使って入れたnode.jsで npx flow などでflowを起動すると以下のようなエラーが出て起動に失敗する

Launching Flow server for /Users/sogasawara/progate
Spawned flow server (pid=6001)
Logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.log
Monitor logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.monitor_log
Launching Flow server for /Users/sogasawara/progate
Spawned flow server (pid=6008)
Logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.log
Monitor logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.monitor_log
Launching Flow server for /Users/sogasawara/progate
Spawned flow server (pid=6012)
Logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.log
Monitor logs will go to /private/tmp/flow/zSUserszSsogasawarazSprogate.monitor_log
Lost connection to the flow server (0 retries remaining): -Out of retries, exiting!

ログ

[2021-01-27 17:22:16.301] argv=/Users/sogasawara/progate/node_modules/flow-bi
Unhandled exception: Unix.Unix_error(Unix.EINVAL, "ftruncate", "")
Raised by primitive operation at file "hack/heap/sharedMem.ml", line 82, char
Called from file "hack/heap/sharedMem.ml", line 146, characters 5-26

環境

Mac Mini(M1), node v10.16.3 (rosetta), flow 0.90.0

解決策(workaround)

.flowconfigに以下のオプションを追加する

[options]
sharedmemory.heap_size=2147483648

なお、公式のissueに問題の対応方法が書いてあります
https://github.com/facebook/flow/issues/8538

heap_sizeはマシンのメモリによって変わるとか。

私のマシンはMac miniのメモリ16GBですが上記の値で動きましたがマシンによって調整が必要そうです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Electronアプリの作り方

はじめに

Electronという、ChromiumとNode.jsを使ったWeb技術でデスクトップアプリを作ることができるフレームワークを使います。

最初にやること

Node.jsをインストールする

Node.jsをインストールします。
https://nodejs.org/ja/download/
から、自分のOSを選びます。

詳しくはこちらから。
Node.jsをインストールすることでElectronアプリを作ることができます。

フォルダ作成

アプリを作成するためのフォルダを作成します。
今回は、Electronとしました。

$ cd フォルダのパス

としてフォルダを指定する。

package.jsonを作成する

$ npm init -y
package.json
{
  "name": "Electron",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

このようなファイルが作成される。
アプリのバージョンを変更するには、"version": "1.0.0" を変更する。

Electronをインストールする

$ npm i -D electron

結構待たされる。

srcフォルダ等作成

srcフォルダを作り、その中に

  • index.html
  • main.js
  • package.json

を作成。

index.htmlはご自由に書いてください。
今回はHello worldを表示します。

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Electron</title>
  </head>
  <body>
    <h1>Hello world</h1>
  </body>
</html>
main.js
const { app, Menu, BrowserWindow } = require('electron');
const path = require('path');
const url = require('url');

let mainWindow;

function createWindow() {
    mainWindow = new BrowserWindow({width: 1200, height: 675, 'icon': __dirname + 'favicon.ico'})

    mainWindow.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
    }));


    mainWindow.on('closed', () => {
        mainWindow = null;
    });
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', () => {
    if (mainWindow === null) {
        createWindow();
    }
});

'icon': __dirname + 'favicon.ico' でアイコンを指定している。なくても良い。

package.json
{
  "main": "main.js"
}

main.jsを指定

試しに起動してみる

$ npx electron src

スクリーンショット (19).png

Hello worldが表示されました。

パッケージング

electron-packagerを使ってパッケージングする

electron-packagerのインストール

$ npm i -D electron-packager

それでは、パッケージングしていきます。
windows向けです。

$ npx electron-packager src electron --platform=win32 --arch=x64 --overwrite --icon=src/favicon.ico

ここでもアイコンを指定しています。

Electronのデザイン

全画面&タイトル、メニューバーなし

main.js
mainWindow = new BrowserWindow({kiosk: true, 'fullscreen': true, 'frame': false});

スクリーンショット (20).png

main.jsのnew BrowserWindowを指定しているところに追加する
展示用などにおすすめ。
Ctrl+Wでアプリがシャットダウンされる。

メニューバーなし

main.js
mainWindow.setMenu(null);

new BrowserWindowのあとに追加する。
スクリーンショット (21).png

とてもシンプルで良い。

パッケージングを楽に

package.json
"scripts": {
    "start": "electron ./src",
    "macos": "electron-packager ./src electron --platform=darwin --arch=x64 --overwrite --icon=src/favicon.ico",
    "windows": "electron-packager ./src electron --platform=win32 --arch=x64 --overwrite --icon=src/favicon.ico"
  }

と、追加しておきます。
このとき、忘れずに"start"の前に,を追加してください。

$ npm run windows

とするとかんたんにパッケージングできます。
windowsをmacosに変えるとMacOS向けにパッケージングされます。

まとめ

Web技術でアプリを作ることができました。
サクっとアプリが作れるいい時代ですね。

参考

https://qiita.com/y-tsutsu/items/179717ecbdcc27509e5a

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ペアリングしてステレオ化したGoogle Home(Nest)にボイスコマンドでPodcastを流してもらう

2台をペアリングしてステレオスピーカー化したGoogle Nest mini(Google Home miniの新型)にボイスコマンドでPodcastをしゃべってもらおうと思ったら、意外とハマりどころが多く、結構な苦戦を強いられました。
苦労の末、どうにかやりたいことはできるようになったので、今回はその辺の話を書きたいと思います。
実現のためにいくつかのクラウドサービスを利用していますが、無料枠の範囲内で大丈夫です。

この辺の記事を参考にしています。

Google Homeで好きなポッドキャストをスマートに再生する
google-home-notifier で スピーカーグループを喋らせる

やりたいこと

google homeに「ok google、~~~を流して」とお願いすると、あらかじめ指定しておいたpodcastの最新話を再生してくれる。

必要なもの

  • IFTTTのアカウント
  • firebaseのプロジェクト(無料プランで大丈夫です)
  • 自宅のLAN内で常時インターネットに接続されていてnodejsが動作するPCなど
    • 僕はRaspberry pi を使っています

動作させる仕組み

  • Google home(Google Assistant)へのボイスコマンド入力を、IFTTTでフックする。
  • IFTTTのアクションでfirebaseのRealtime Databaseを更新する。
  • nodejsで組んだスクリプトでfirebaseの更新を監視しておき、更新を検知したらPodcastのRSSを取得&パースし、最新話のMP3のURLをGoogle Homeに渡す。
  • Google Homeが、受け取ったMP3を再生してくれる

実現手順

大まかな手順は「Google Homeで好きなポッドキャストをスマートに再生する」に書かれている通りですが、いくつか修正した方がよい箇所があります。

元記事からの変更点

環境によっては、いくつかの修正が必要です。

mdnsモジュールの一部機能がRaspberryPiで動かない問題の修正

依存ライブラリのgoogle-home-notifierの内部で利用されている「mdns」モジュールが、Raspberrypiでは正しく動作しないため、修正が必要です。

121行目を以下のように修正します

変更前

~/node_modules/mdns/lib/browser.js
, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo()

変更後

~/node_modules/mdns/lib/browser.js
, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo({families:[4]})

※ 引数に {families:[4]} を追加しています。

元記事に掲載されているRaspberrypiで動かすnodejsスクリプトの修正

podcastを再生するGoogle HomeをIP Addressで指定していますが、スピーカーのペアリングやグループ化をしているとうまく動きません。

IPアドレス指定ではなく、google-home-notifierは、対象のgoogle homeデバイスをIPアドレス指定ではなく内部名で指定することもできるようになっているため、内部名で指定するように変更します。

まず、再生したいGoogle Home(Nest)の内部名を調べます。

internal_name.js
var mdns = require('mdns');
var browser = mdns.createBrowser(mdns.tcp('googlecast'));
browser.start();
browser.on('serviceUp', function(service) {
  console.log('Device "%s" at %s:%d', service.name, service.addresses[0], service.port);
});

実行結果は以下のようになります(デバイスの内部名の一部を伏字にしています)。

*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html>
*** WARNING *** The program 'node' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html>
Device "AQUOS-TV***************" at 192.168.0.111:8009
Device "Google-Home-Mini***************" at 192.168.0.22:8009
Device "Google-Nest-Mini***************" at 192.168.0.20:8009
Device "Google-Cast-Group***************" at 192.168.0.20:32000
Device "Google-Nest-Mini***************" at 192.168.0.21:8009

出力が終わったらCtrl+Cで終了

(グループ化・ペアリングしたスピーカーの場合には、Google-Cast-Group-**** のような名前になります)

上で取得した情報をもとに、引用元記事で書かれているraspberrypiで動かすスクリプトを以下のように修正&保存します(nodeのバージョンにもよるかもしれませんが、実行時エラーが出たのでそこも修正しています)。

raspberrypi.js
var FeedParser = require('feedparser');
var firebase = require('firebase');
var googleHome = require('google-home-notifier');
var request = require('request');

const lang = 'ja';

// const ip = '192.168.0.20'; //再生したいGoogle HomeのIPアドレス
// googleHome.ip(ip, lang);
const deviceName = 'Google-Cast-Group*************';
googleHome.device(deviceName, lang);

const config = {
  apiKey: 'hoge',
  authDomain: 'fuga.firebaseapp.com',
  databaseURL: 'https://**************.firebaseio.com',
  projectId: 'fuga',
  storageBucket: '',
  messagingSenderId: 'piyo'
};
firebase.initializeApp(config);

var db = firebase.database();
var ref = db.ref('/');
ref.on('child_changed', function(snapshot) {
  // var url = ref.child('url').val();
  var url = snapshot.val();
  if (url) {
    playLatestPodcast(url);
  }
  ref.update({'podcast_url': ''}); // 変更をリセット
});

function playLatestPodcast(url) {
  var req = request(url);
  var parser = new FeedParser();
  var items = [];

  req.on('response', function(res) {
    this.pipe(parser);
  });

  parser.on('readable', function() {
    while (item = this.read()) {
      items.push(item);
    }
  });

  parser.on('end', function() {
    googleHome.play(getLatestPodcastUrl(items), function(notifyRes) {});
  });
}

function getLatestPodcastUrl(items) {
    for (item of items) {
      for (enclosure of item.enclosures) {
        var url = enclosure['url'];
        if (url) {
            return url;
        }
      }
    }
    return "";
}

グループ化したスピーカーに対応させる。

再生するデバイスが1台のgoogle homeの場合には、これでうまくいきますが、Google homeがグループ化されていたり、ペアリングしているステレオ化されたスピーカーで再生しようとしている場合はうまく動きません。

google-home-notifier で スピーカーグループを喋らせる」の記事を参考に、~/node_modules/google-home-notifier/google-home-notifier.js を修正します。

変更前

~/node_modules/google-home-notifier/google-home-notifier.js
deviceAddress = service.addresses[0];

変更後

~/node_modules/google-home-notifier/google-home-notifier.js
deviceAddress = {};
deviceAddress.host = service.addresses[0];
deviceAddress.port = service.port;

※ 修正が必要な個所は2か所あります。

これで、作業はすべて完了です。

IFTTTで設定した「ok google、~~~~を流して」というフレーズを言えば、目的のpodcastの最新話が自動で再生されるかと思います。

同じ仕組みを利用して、miniDLNAなどで組んだDLNAサーバのプレイリストURLをgoogle homeに渡してあげれば、「ok google、NASの音楽を流して」という音声コマンドでNASに保存した音楽をシャッフル再生してくれるような仕組みなども作れそうです。

機会があればやってみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ペアリングしてステレオ化したGoogle Home(Nest)に、ボイスコマンドでPodcastを流してもらう

2台をペアリングしてステレオスピーカー化したGoogle Nest mini(Google Home miniの新型)にボイスコマンドでPodcastをしゃべってもらおうと思ったら、意外とハマりどころが多く、結構な苦戦を強いられました。
苦労の末、どうにかやりたいことはできるようになったので、今回はその辺の話を書きたいと思います。
実現のためにいくつかのクラウドサービスを利用していますが、無料枠の範囲内で大丈夫です。

この辺の記事を参考にしています。

Google Homeで好きなポッドキャストをスマートに再生する
google-home-notifier で スピーカーグループを喋らせる

やりたいこと

google homeに「ok google、~~~を流して」とお願いすると、あらかじめ指定しておいたpodcastの最新話を再生してくれる。

必要なもの

  • IFTTTのアカウント
  • firebaseのプロジェクト(無料プランで大丈夫です)
  • 自宅のLAN内で常時インターネットに接続されていてnodejsが動作するPCなど
    • 僕はRaspberry pi を使っています

動作させる仕組み

  • Google home(Google Assistant)へのボイスコマンド入力を、IFTTTでフックする。
  • IFTTTのアクションでfirebaseのRealtime Databaseを更新する(webhookを利用)。
  • 自宅LAN環境内に配置したnodejs製スクリプトでfirebase realtime databaseの更新を監視しておき、更新を検知したらPodcastのRSSを取得&パースし、podcast最新話のMP3URLをGoogle Homeに渡す。
  • Google Homeが、受け取ったMP3を再生してくれる

実現手順

大まかな手順は「Google Homeで好きなポッドキャストをスマートに再生する」に書かれている通りですが、いくつか修正した方がよい箇所があります。

元記事からの変更点

環境によっては、いくつかの修正が必要です。

mdnsモジュールの一部機能がRaspberryPiで動かない問題の修正

依存ライブラリのgoogle-home-notifierの内部で利用されている「mdns」モジュールがRaspberrypiでは正しく動作しないため、修正が必要です。

~/node_modules/mdns/lib/browser.js```の121行目を以下のように修正します
__変更前__

```~/node_modules/mdns/lib/browser.js
, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo()

変更後

~/node_modules/mdns/lib/browser.js
, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo({families:[4]})

※ 引数に {families:[4]} を追加しています。

元記事に掲載されているRaspberrypiで動かすnodejsスクリプトの修正

元記事に掲載されているスクリプトでは、podcastを再生するGoogle HomeをIP Addressで指定していますが、この方法だとスピーカーのペアリングやグループ化をしているとうまく動きません。

google-home-notifierは、対象のgoogle homeデバイスをIPアドレス指定ではなく内部名で指定することもできるようになっているため、再生するデバイスを内部名で指定するようにスクリプトを変更します。

それには、対象のgoogle homeの内部名を調べなければなりません。

下記のようなスクリプトを用意し、nodeで動作させます。

internal_name.js
var mdns = require('mdns');
var browser = mdns.createBrowser(mdns.tcp('googlecast'));
browser.start();
browser.on('serviceUp', function(service) {
  console.log('Device "%s" at %s:%d', service.name, service.addresses[0], service.port);
});

実行結果は以下のようになります(デバイスの内部名の一部を伏字にしています)。

*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html>
*** WARNING *** The program 'node' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html>
Device "AQUOS-TV***************" at 192.168.0.111:8009
Device "Google-Home-Mini***************" at 192.168.0.22:8009
Device "Google-Nest-Mini***************" at 192.168.0.20:8009
Device "Google-Cast-Group***************" at 192.168.0.20:32000
Device "Google-Nest-Mini***************" at 192.168.0.21:8009

出力が終わったらCtrl+Cで終了させてください

(グループ化・ペアリングしたスピーカーの場合には、Google-Cast-Group-**** のような名前になります)

上で取得した情報をもとに、元記事で書かれているraspberrypiで動かすスクリプトを以下のように修正&保存します(nodeのバージョンにもよるかもしれませんが、実行時エラーが出たのでそこも修正しています)。

raspberrypi.js
var FeedParser = require('feedparser');
var firebase = require('firebase');
var googleHome = require('google-home-notifier');
var request = require('request');

const lang = 'ja';

// const ip = '192.168.0.20'; //再生したいGoogle HomeのIPアドレス
// googleHome.ip(ip, lang);
const deviceName = 'Google-Cast-Group*************';
googleHome.device(deviceName, lang);

const config = {
  apiKey: 'hoge',
  authDomain: 'fuga.firebaseapp.com',
  databaseURL: 'https://**************.firebaseio.com',
  projectId: 'fuga',
  storageBucket: '',
  messagingSenderId: 'piyo'
};
firebase.initializeApp(config);

var db = firebase.database();
var ref = db.ref('/');
ref.on('child_changed', function(snapshot) {
  // var url = ref.child('url').val();
  var url = snapshot.val();
  if (url) {
    playLatestPodcast(url);
  }
  ref.update({'podcast_url': ''}); // 変更をリセット
});

function playLatestPodcast(url) {
  var req = request(url);
  var parser = new FeedParser();
  var items = [];

  req.on('response', function(res) {
    this.pipe(parser);
  });

  parser.on('readable', function() {
    while (item = this.read()) {
      items.push(item);
    }
  });

  parser.on('end', function() {
    googleHome.play(getLatestPodcastUrl(items), function(notifyRes) {});
  });
}

function getLatestPodcastUrl(items) {
    for (item of items) {
      for (enclosure of item.enclosures) {
        var url = enclosure['url'];
        if (url) {
            return url;
        }
      }
    }
    return "";
}

グループ化したスピーカーに対応させる。

再生するデバイスが1台のgoogle homeの場合には、これでうまくいきますが、Google homeがグループ化されていたり、ペアリングしているステレオ化されたスピーカーで再生しようとしている場合はうまく動きません。

そこで、「google-home-notifier で スピーカーグループを喋らせる」の記事を参考に、~/node_modules/google-home-notifier/google-home-notifier.js を修正します。

変更前

~/node_modules/google-home-notifier/google-home-notifier.js
deviceAddress = service.addresses[0];

変更後

~/node_modules/google-home-notifier/google-home-notifier.js
deviceAddress = {};
deviceAddress.host = service.addresses[0];
deviceAddress.port = service.port;

※ 修正が必要な個所は2か所あります。

これで、作業はすべて完了です。

IFTTTで設定した「ok google、~~~~を流して」というフレーズを言えば、目的のpodcastの最新話が自動で再生されるかと思います。

このスクリプトを常時動かしておけば、google homeに「ok google ~~を流して」とお願いするだけで、目的のpodcastの最新話を聞くことができるようになります。

同じ仕組みを利用して、miniDLNAなどで組んだDLNAサーバのプレイリストURLをgoogle homeに渡してあげれば、「ok google、NASの音楽を流して」という音声コマンドでNASに保存した音楽をシャッフル再生してくれるような仕組みなども作れそうです。

機会があればやってみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nodeで、SJIS文字コードで、かつ、行ごとに列数が変動するcsvを読み込んでみた

NodeでSJIS文字コードで、かつ列数が行ごとに変動するcsvを読み込んで使う必要があるため、
試行錯誤して実装できたコードを残しておきます。
結構試行錯誤して面倒だったので、どなたかの参考になれば幸いにて。

const csv = require('csv')
const csvFilePath = './path/to/csv_file.csv'
const fs = require('fs')
const iconv = require('iconv-lite')

/**
 * csvデータの取得(SJISのファイル)
 */
const getCsv = async function() {
  return new Promise(resolve => {
    fs.readFile(csvFilePath, function (err, data) {//非同期処理なのでreadFileSyncではなくreadFile
      if (err) throw err
      const buf = Buffer.from(data, 'binary')//new BufferはdeprecatedなのでBuffer.fromを使う
      const ret = iconv.decode(buf, 'Shift_JIS')//SJISをdecodeする
      csv.parse(ret, {relax_column_count: true},//列数変動ファイルの読込オプション
        function (err, output) {
          resolve(output)
        })
    })
  })
}

async function main() {
  const data = await getCsv()//非同期読込
  console.log(data)//utf-8でのデータ取得
  console.log(data.length)//行数の確認
}

main()//実行

参考:オプション設定で参考になったページ
csv parser本家ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nodeで、SJISで、かつ、行ごとに列数が変動するcsvを読み込んでみた

Nodeにて、SJISで、かつ列数が行ごとに変動するcsvを読み込んで使う必要があったため、
試行錯誤して実装できたコードを残しておきます。

いろいろな参考コードがネットにありましたが、
要件そのままのものは見つからず、
結構試行錯誤しました。
どなたかの参考になれば幸いにて。

const csv = require('csv')
const fs = require('fs')
const iconv = require('iconv-lite')

const csvFilePath = './path/to/csv_file.csv'

/**
 * csvデータの取得(SJISのファイル)
 */
const getCsv = async function() {
  return new Promise(resolve => {
    fs.readFile(csvFilePath, function (err, data) {//非同期処理なのでreadFileSyncではなくreadFile
      if (err) throw err
      const buf = Buffer.from(data, 'binary')//new BufferはdeprecatedなのでBuffer.fromを使う
      const ret = iconv.decode(buf, 'Shift_JIS')
      const ret2 = iconv.encode(ret, 'UTF-8')
      csv.parse(ret2, {relax_column_count: true},//列数変動ファイルの読込オプション
        function (err, output) {
          resolve(output)
        })
    })
  })
}

async function main() {
  const data = await getCsv()//非同期読込
  console.log(data)
  console.log(data.length)
}

main()

それにしてもNode.jsって便利で面白いですね。

参考:オプション設定で参考になったページ
csv parser本家ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Raspberry pi zero w に node.js をインストール

はじめに

Raspberry pi zero w に node.js をインストールしたくなった。Raspberry pi zero w をサーバーにし、React.jsでアプリケーションを作ってLAN内の別端末から利用したい、というのがその理由だ。気を遣う部分は、今回の対象が、「Raspberry pi zero である」という点である。RaspberryPi3や4とはCPUが違うのだ。Raspberry pi zero に使われているCPUはArmv6, RaspberryPi3や4はArmv7だそうで・・・。あと、Rasberry pi OS は 32bitOS ですよね。

準備

Raspberry pi zero w を Raspberry pi OS lite でセットアップし、まずはインターネットにつながっているLANまでの接続は行った。apt のアップデートができれば、準備オッケーというところではないだろうか。
(この下準備については、前回記事等が参考になるはず・・・。ちないに今回の案件は、前回の4GBディスクとは別のSDカードでRaspberry Pi zero をセットアップ。)

node.js インストール

いつものごとく、諸先輩方の知見を参照する。

  【Node.js】Raspberry Pi Zeroに最新のNode.jsをインストールする
  https://www.taneyats.com/entry/install-nodejs-on-raspberrypi-zero

上述先輩の記録資料を真似して、OS,CPUに合うソースファイルを公式から取得することにする。

ファイルの取得

見てみると、node-v12 以降は、Armv6のファイルが無いようだ・・・(2021年1月時点)。また、node-v11だと、あとでeslint-typescriptライブラリをインストールできない(create-react-appでReact環境構築について)。そこで、次のとおりファイルを取得する。

# まずは任意のディレクトリを作成(取得ソース展開用)
$ mkdir node_src

$ cd node_src
$ wget https://nodejs.org/dist/latest-v10.x/node-v10.23.2-linux-armv6l.tar.gz
$ tar -zxf node-v10.23.2-linux-armv6l.tar.gz

インストール

先達に倣って、/usr/local にディレクトリ丸ごとコピーして使えるようにする。

$ cd node-v10.23.2-linux-armv6l
$ rm CHANGELOG.md LICENSE README.md
$ sudo cp -R * /usr/local/

確認

コマンドのバージョンを確認してみる。

$ node -v
v10.23.2

npm のバージョンを上げておく。

$ sudo npm install -g npm

$ npm -v
6.14.10

備考

yarn コマンドをインストールする。

$ sudo npm install -g yarn

おわりに

Raspberry pi zero w に node.js をセットアップした。作業時間は1時間くらい。通信量もさほどなく、コンパイル等も不要だったため、コマンドを打って待つ時間もさほどなかった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[エラー対処] Express チュートリアル populatedbのURLが機能しない

Express チュートリアルパート3: データベースの使用 (Mongooseを使用)

node populatedb <your mongodb url>

がつまづきポイントでして

your mongodb url に以下のyour_user_name_passwordの部分を変更してコマンドを実行すると思いますが、失敗し時間を取られたので、備忘録として残しておきたいと思います。

mongodb+srv://your_user_name:your_password@cluster0.a9azn.mongodb.net/local_library?retryWrites=true

Exampleとして

  • password → iekud

など

失敗する例として

  • dbUserPassword 推測ですが、デフォルトの文字列だと失敗する?
  • 12345 数字だけでは失敗するらしい?

失敗するがご存知の方がいらっしゃれば、コメントをしていただけると嬉しいです!

参考ページ
Express Tutorial Part 3: Using a Database (with Mongoose)
node populatedb url not working

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsの基本 その2

Node.js備忘録として、書き始めました。その1はこちら
今回はほぼexpressの基本みたいな回です。

HTTP

HTTPはインターネットで支配的なプロトコルです。Node.jsはサーバー側クライアント側双方に適したモジュールを持っています。
Node.jsに興味を持っている人なら多くの場合express.jsというウェブアプリケーションフレームワークが存在するのを知っていると思います。expressはとても便利で僕も一度覚えてしまうともう他へ浮気できなくなってしまいました。koaやhapiなどのオルタナティブも存在しますが、Qiitaにおけるexpress.js記事の数は他の追随を許していませんから、初学者はexpress一択でしょう。
しかし、何にせよ基礎の基礎を知っておくのは大事なことです。ビルトインモジュールであるhttpを使ったサーバーとクライアントの書き方を見てみましょう。
その1でも用いたHello Worldの例は最も簡素なNode.jsを用いたHTTPサーバーです。

server.js
const http = require('http')
http.createServer((req, res) => {
  res.end("Hello World!")
}).listen(3000, "127.0.0.1")

httpを用いたルーティング

server.js
const http = require('http')
const url = require('url').URL
const port = 3000
const hostName = "127.0.0.1"

http.createServer((req, res) => {
  const path = new url(req.url, `http://${hostName}:${port}`)
  if(path.pathname === "/"){
    res.writeHead(200, {'Content-Type': 'text/plain'})
    res.end('This is root page\n')
  } else if(path.pathname === "/about"){
    res.writeHead(200, {'Content-Type': 'text/plain'})
    res.end('this is about page\n')
  } else {
    res.writeHead(404, {'Content-Type': 'text/plain'})
    res.end('404姉さん')
  }

}).listen(port, hostName)

console.log(`Server running at http://${hostName}`)

HTTPクライアント

client.js
const http = require('http')

http.get('http://www.google.co.jp', (res) => {
  if(res.statusCode == 200){
    console.log('successful')
  } else {
    console.log('google is down again')
  }
}).on('error', error => {
  console.log(error)
})

Express.js

Express.jsは最も採用率の高いウェブアプリケーションフレームワークです。これさえあればウェブアプリケーションを作成する際、まずほとんどの状況に対応できる機能が備わっています。
まずはインストールしてみましょう。

npm install -g express

これで環境のどこでもexpressのジェネレーター機能を使えるようになります。

express [web app name]

このコマンドで雛形を作成できます。初学ではどのようなモジュールを用意するべきかわかりやすいでしょう。

起動

cd [web app name]
npm install
node app.js

作成されたアプリのディレクトリを見ていきましょう。(express@4.17.1)

node_modules

インストールされているモジュールをここに保持しています。

package.json

ざっくり言えばこのウェブアプリケーションに関しての情報を保持しているファイルです。

binフォルダ

実行用のファイルを入れるためのフォルダです。

routes

ルーティング用のフォルダです。RESTfulなAPIを作成する上でルーティングは可視化面でもメンテナンス面でも重要で、ユーザーに関しての処理はすべて/userルートへ認証に関しての処理はすべて/authルートへなど個別のexpress.route関数ファイルを用意するのが良いでしょう。次の項目でより詳しく説明します。

public

スタイルシートやjavascriptファイル、画像などをこのフォルダに保存してアプリケーションにここを参照させるようにします。例としてはbootstrapやjQuerryなどでしょう。

views

ビューエンジンに指定された形式の同名ファイルを参照します。express generatorはjadeを使用しています。(個人的には少し可読性が低いにしろejsが好きです)
jadeというのはテンプレートエンジンで、いわば雛形です。動的なウェブページのレンダリングに向いています。

ウェブアプリケーションにおけるルーティング

ルーターの設定は少なくともNode.jsでウェブアプリケーションを作成するうえでとても重要です。どのHTTPリクエストにどのように処理するかの振り分けがルーティングです。

HTTPメソッド

Express.jsはルート定義の際にhttpメソッドとパスを組み合わせて使用します。
GET, POST, PUT, DELETE, などなど
手始めにはサーバーからデータを受け取るGETとサーバーへデータを送るPOSTを使用するところからでしょう。

GETとPOSTのリクエストのテストを例を書きますが、それに先立ってexpressのミドルウェアでHTTPリクエストの必要な情報を抽出してくれるbody-parserというモジュールをインストールしましょう。

npm install body-parser
server.js
const express = require('express')
const bodyParser = require('body-parser')
const server = express()
const port = 3000
server.use(bodyParser.urlencoded({extended:true}))
server.use(bodyParser.json())
//root
server.get('/', (req, res) => {
  res.send('hello world')
})
//about get route
server.get('/about', (req, res) => {
  res.send('about route get request called')
})
//about post route
server.post('/about', (req, res) => {
  console.log('called')
  const requestBody = req.body
  console.log(requestBody)
  res.send(req.body)
})

server.listen(port, () => {
  console.log('server listening on:' + port)
})

ルートにおけるパラメータの扱い方

アプリケーションによってはRESTfulであるためにパスからパラメータを受け取る必要があります。expressにおいてはパスの最後に":[parameter_name]"で定義できます。

app.get('/api/users/:id', (req, res) => {
  res.send('受け取ったIDは' + req.params.id + 'です')
})

クエリの扱い方

アプリケーションによってはクエリによる検索機能などを導入する必要があるでしょう。
Expressにおいてはパスに"?[query_parameter_name]=[query_value]"で定義されたHTTPコールを

req.query.query_parameter_name

でその値を扱うことができます。

server.get('/search/', (req, res) => {
  res.send(req.query)
})

Routeを外部モジュールとして扱う

小規模なアプリの構築には必ずしも要るとは限りませんが、可読性の向上のため、たとえばユーザーについての処理はすべて/userのルートへ、認証のための処理はすべて/authのルートへ、それぞれ個別のファイルとしてまとめて大元のapp.jsへエクスポートするのが推奨されるでしょう。

route/users.js
var express = require('express');
var router = express.Router();

router.get('/', function(req, res) {
  res.send('/users called');
});

router.get('/all', (req, res) => {
  res.send('/users/all called')
})

module.exports = router;

このルーターをserver.jsでインポートしましょう

server.js
const express = require('express')
const bodyParser = require('body-parser')
const usersRoute = require('./routes/users')
const server = express()
const port = 3000
//users route
server.use('/users', usersRoute)
//root
server.get('/', (req, res) => {
  res.send('hello world')
})

server.listen(port, () => {
  console.log('server listening on:' + port)
})

ビューレンダリング

先程からずっと例に使っているres.send()という関数は受け取った値をテキスト及びHTMLとしてクライアントへ送ります。
その他に例えばres.json()という関数は受け取った値を有効なJSONファイルの形式で送信します。
res.render()という描画されてHTMLを返す関数に用いる、ビューテンプレートは非常に便利な機能です。たとえばブログを作成するとして、記事ごとにHTMLを書くのは冗長ですし非効率です。タイトルや本文のデータを受け取ってどのように当てはめて描画するかの雛形を作っておくことで、"/archives/:article_id" で対応するidの記事タイトルや本文を受け渡しするだけでよくて効率的です。

ビューテンプレートエンジンと呼ばれるものは色々あります。僕は個人的にejsをプロジェクトで使ってきたので覚えがありますが、ここではexpress generatorに用いられているjadeで例を出していきましょう。

まずjadeのモジュールをインストールしましょう

npm install jade

次にviewsフォルダを作成しindex.jadeファイルを作成しましょう

index.jade
doctype html
html
  head
    title Jade Example
  body
    h1 #{message}

jadeはインデントセンシティブなので開業とtabによる間隔で階層を形成する必要があり、閉じるためのタグがないHTMLを書いているような感じです。受け渡されたデータは#{}の中で変数名を指定します。

server.js
const express = require('express')
const bodyParser = require('body-parser')
const server = express()
const port = 3000
//set jade as its view engine
server.set('view engine', 'jade')
//root path
// render index.jade with message = 'hello world!' 
server.get('/', (req, res) => {
  res.render('index', {message:"Hello World!"})
})

server.listen(port, () => {
  console.log('server listening on:' + port)
})

次へ

その3へ続きます。主にmongoDBに関してになると思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js+TypeScriptでマルチプロセス対応カスタムサーバ作成

Next.js+TypeScriptでマルチプロセス対応カスタムサーバ作成

カスタムサーバ

 Next.jsはWebServer機能を標準で内蔵していますが、マルチプロセスや特殊なセッション処理などを組み込む場合には、カスタムサーバという形でWebServer部分を自分で実装する必要があります。

 公式にサンプルはある物の、以外に日本語の情報が少ない、それどころかマルチプロセスやfastifyでの実装記事は皆無だったので、書いていきたいと思います。

マルチプロセス化について

 Next.jsを動かしているNode.jsは基本的にシングルスレッドで動作します。シングルスレッドといってもI/Oアクセスに関しては非同期で行われているため、無駄なブロックは起こらず、実用的な速度で動作することが可能です。

 ところが計算処理などをしている間は当然他の仕事は出来ません。マルチコアCPUなどでハードウエア的に余裕があっても、シングルスレッドである限りはせっかくのリソースが活用できないのです。

 これに対処するにはNext.jsをマルチスレッドではなく、マルチプロセス化するのが有効な手段となります。ありがたいことにNode.jsには、マルチプロセス化を簡単に実装するライブラリが標準提供されているので、カスタムサーバ化のコードを少し書くだけで、その恩恵を受けることが出来ます。

Fastifyに関して

 Node.jsでWebServer機能を実装するフレームワークとして有名なのはExpressです。しかし古い実装を引きずっているため、応答速度が遅いといわれています。今回はベンチマークで上位に位置するFastifyを使ってカスタムサーバを作ります。

インストールが必要な最低限のパッケージ

yarn add cross-env fastify next react react-dom
yarn add -D @types/node @types/react @types/react-dom ts-node-dev typescript

カスタムサーバの実装コード

 以下の二つのファイルを用意します。
 ちなみに環境変数でINDEXというのを子プロセスに渡していますが、workerにIDが振られるので実は無くてもかまいません

server/index.ts
import next from "next";
import * as os from "os";
import * as cluster from "cluster";
import { parse } from "url";
import fastify from "fastify";

const dev = process.env.NODE_ENV !== "production";
const clusterSize = Math.min(os.cpus().length, 4);
const portNumber = 3000;

if (cluster.isMaster) {
  for (let i = 0; i < clusterSize; i++) cluster.fork({ INDEX: i });
} else {
  const app = next({ dev });
  const handle = app.getRequestHandler();
  const server = fastify();
  app.prepare().then(() => {
    server.all("*", (req, res) => {
      return handle(req.raw, res.raw, parse(req.url, true));
    });
    server.listen(portNumber).then(() => {
      console.log(`[${process.env.INDEX}]:http://localhost:${portNumber}`);
    });
  });
}
server/tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "../.next",
    "esModuleInterop": true
  }
} 

 tsconfig.jsonを作成しているのは、Next.js管理下のpagesファイルなどとはTypeScriptのビルドの扱いが異なるからです。

スクリプト関係

 devはカスタムサーバ自体の自動リロードのため、ts-node-devを使っています。ただし.nextの中身はNext.js側が調整するので、無視指定が必要です。

package.json
{
  "name": "nextjs-custom",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "ts-node-dev --ignore-watch \\.next -P server/tsconfig.json server/index.ts",
    "build": "tsc -b server && next build",
    "start": "cross-env NODE_ENV=production node .next/index.js",
    "export": "next export"
  },
  "devDependencies": {
    "@types/node": "^14.14.22",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "ts-node-dev": "^1.1.1",
    "typescript": "^4.1.3"
  },
  "dependencies": {
    "cross-env": "^7.0.3",
    "fastify": "^3.11.0",
    "next": "^10.0.5",
    "react": "^17.0.1",
    "react-dom": "^17.0.1"
  }
}

まとめ

 大したコード量も必要なくカスタムサーバが実装できました。マルチプロセスとFastifyのパワーによって、きっと快適SSRライフが送れることでしょう。

 ただしベンチマークを取った結果、それなりの負荷をかけても効果が顕著に出るのは2プロセスまでというオチでした。ベンチマークに関しては別記事を書く予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MyAnimListのアニメランキングをグラフ入りMDにする

使用例

個人的に、はてなブログで使っています。投稿する部分は割愛。

レポジトリ

https://github.com/and0ry0/myanimelist-email

前提

package.json
"dependencies": {
    "json2md": "^1.9.2",
    "node-fetch": "^2.6.1",
  }

アニメオブジェクト

index.js
const fetch = require('node-fetch')

function convertAnime(anime) {
  const jaTitle = anime.title
    .replace('Shingeki no Kyojin', '進撃の巨人')
    // 日本語翻訳適当ですいません

  return {
    rank: anime.rank,
    url: anime.url,
    title: jaTitle,
    score: anime.score,
    start: anime.start_date,
    image_url: anime.image_url,
    members: anime.members
  }
}

MDに変換

index.js
const convertToMd = (anime) => {
  return [
    { h2: anime.rank + '. ' + anime.title + ' (' + anime.members.toLocaleString('ja-JP') + '人視聴)' },
    { link: { title: 'MyAnimeListで詳細を見る', source: anime.url } },
    { addImage: { title: 'MyAnimeListのサムネ', source: anime.image_url } }
    {
      ul: [
        '視聴者数: **' + anime.members.toLocaleString('ja-JP') + '人**',
        'スコア: **' + anime.score,
        '放送開始時期: ' + anime.start,
      ]
    },
    { p: '' /* 改行です */}
  ]
}

Jikan APIでランキング取得

詳しくはJikanのDocsを参照。

index.js
// Get anime ranking
async function MalRank() {

  // Jikan API https://jikan.docs.apiary.io/
  const res = await fetch('https://api.jikan.moe/v3/top/anime/1/bypopularity')
  const data = await res.json()

  // アニメの配列を作る
  const topAnimes = data.top.map((anime) => convertAnime(anime))

  const firstAnime = {
    title: topAnimes[0].title,
    members: topAnimes[0].members
  }

  const json2md = require('json2md')

  // 画像とグラフを用意する
  json2md.converters.addImage = function ({ title, source }) {
    return '<img width="150px" title="' + title + '" src="' + source + '" />'
  }
  json2md.converters.animeGraph = function ({ rank, title, members }) {
    // 相対的に長さを作る
    const relativeWidth = (members - topAnimes[10].members) / (firstAnime.members - topAnimes[10].members)
    return '<div class="graphBox"><div style="width: ' + relativeWidth * 100 + '%;" class="title">' + rank + '. ' + title + '</div><div class="members">' + members.toLocaleString('ja-JP') + '</div></div>'
  }

  // はてなブログ対策のspan https://blog.uchiten.info/entry/2017/01/30/174500
  const style = `<span></span><style>.graphBox{width:100%;display:flex;padding:.3em;margin:0 0 .2em;position:relative}.graphBox>.title{overflow:visible;white-space:nowrap;background:#add8e6;font-weight:700;padding:.3em}.graphBox>.members{color:gray;position:absolute;right:.3em;padding:.3em}</style></span>\n\n`

  const graphMd = `## Top10はこんな感じ \n\n` + json2md(graphJson) + `\n\n11位のアニメの視聴者数を引いて、相対的にグラフを作っています。\n\n`

  const dataJson = topAnimes.map((anime) => convertToMd(anime))

  const mainMd = json2md(dataJson)

  console.log(style + graphMd + mainMd)
}

MalRank();

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む