●Node.jsとVue/CLIの導入
※ Node.js LTS版の導入
$ curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
$ sudo apt-get install -y nodejs
$ node -v
v18.16.1
$ npm -v
9.5.1
※ Vue/CLIの導入
$ npm install -g @vue/cli
$ vue --version
@vue/cli 5.0.8
※ updateの確認
$ sudo npm update -g @vue/cli
changed 1 package in 19s
80 packages are looking for funding
run `npm fund` for details
npm notice
npm notice New minor version of npm available! 9.5.1 -> 9.7.2
npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.7.2
npm notice Run npm install -g npm@9.7.2 to update! ← update方法
npm notice
※ npmのupdate
$ sudo npm install -g npm@9.7.2
removed 17 packages, and changed 69 packages in 9s
28 packages are looking for funding
run `npm fund` for details
$ npm -v
9.7.2
※ プロジェクトの作成
$ cd 任意のフォルダ
$ vue create vue-node
presetの選択は'Manually select features'で、
モジュールを選択では、'Babel','Router','Linter'
Vue.js のバージョン選択は、3.x
Linter の設定選択は、'ESLint with error prevention only','Lint on save'
設定情報の格納先選択で、'In dedicated config files'
設定保存確認でYすると、「vue-node」フォルダが作成される
※ Node.jsのbackend環境準備
$ cd vue-node
$ mkdir bkend
$ cd bkend
$ npm init -y
entry point: (index.js)以外はdefaultでenter
・追加モジュール
$ npm install express
$ npm install python-shell
$ npm install csv-parse
●vue.config.js
vue.config.jsを編集してvueのport指定とbuild時に作成する本番環境用のディレクトリを指定
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/~pi/music-nodevue/' ← 本番用ディレクトリ
: '/',
devServer: {
port: 8082, ← 開発用vue.js port
}
}
●package.json
npm起動コマンドの設定をする
{
"name": "vue-node",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:3000",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --dest music-nodevue", ← 本番用vue.js作成用ディレクトリ指定
"lint": "vue-cli-service lint",
"start": "node bkend/index.js" ← 追記:node.jsの起動用
},
"dependencies": {
"axios": "^1.4.0",
"core-js": "^3.8.3",
"csv-parse": "^5.4.0",
"vue": "^3.2.13",
"vue-router": "^4.0.3"
},
:
:
●main.js router設定
main.jsでrouterの設定を行う
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
●router index.js
vue createした時に作成されたrouter index.js編集
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/noderadio',
name: 'noderadio',
component: () => import(/* webpackChunkName: "noderadio" */ '../views/NodeRadio.vue')
},
{
path: '/nodemfile',
name: 'nodemfile',
component: () => import(/* webpackChunkName: "nodemfile" */ '../views/NodeMfile.vue')
},
{
path: '/nodeutube',
name: 'nodeutube',
component: () => import(/* webpackChunkName: "nodeutube" */ '../views/NodeUtube.vue')
},
{
path: '/nodedlna',
name: 'nodedlna',
component: () => import(/* webpackChunkName: "nodeutube" */ '../views/NodeDlna.vue')
},
{
path: '/editytlist',
name: 'editytlist',
component: () => import(/* webpackChunkName: "editytlist" */ '../views/EditYtList.vue')
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
●各ページの枠
各ページの枠は名前が変わるだけで他は同じ
<template>
<div>
<h1 style='text-stroke: 1px #aa5;'>Radio</h1>
<NodePlayer selname="radio"/>
</div>
</template>
<script>
// @ is an alias to /src
import NodePlayer from '@/components/NodePlayer.vue'
export default {
name: 'NodeRadio',
components: {
NodePlayer,
}
}
</script>
●各ページの共通核
各ページの核となる共通コンポーネント の抜粋
<template>
<div>
<form id='file-list'>
<label>選択</label><br>
<select
id='songlist' size=12 v-model='sel' :disabled='playing' @dblclick='flist' >
<option
v-for='(value) in dispList'
:value='value[1]'
:key='value[0]'
:class='value[3]'
>
{ { value[2] } }
</option>
</select>
</form>
<input v-if="selname == 'mfile' || selname == 'dlna'" class='csubmit top' type="submit" value="TOP" @click='ftop' :disabled='playing'/>
<input v-if="selname == 'mfile' || selname == 'dlna'" class='csubmit open' type="submit" value="開く" @click='fopen' :disabled='playing'/>
<input v-if="selname == 'mfile' || selname == 'dlna'" class='csubmit back' type="submit" value="戻る" @click='fback' :disabled='playing'/>
<input class='csubmit play' type="submit" value="再生" @click='play' :disabled='playing' />
<input class='csubmit stop' type="submit" value="停止" @click='stop' />
:
:
</template>
<script>
// eslint-disable-next-line
/* eslint-disable */
import axios from 'axios'
// @ is an alias to /src
import sassStyles from '@/app.scss'
export default {
name: 'TestPlayer',
props: ['selname'],
data: function(){
return {
page: this.selname, // backend nodeへの開始メッセージ
result : '', //演算結果を格納する変数
myip : "", //現在のip addressを格納する変数
:
:
}
},
beforeUnmount() {//離れたとき
document.body.className = '';
this.$root.headerHide = false;
this.$root.footerHide = false;
},
created() { // vue起動時に実行し事前情報を得る
this.myip = window.location.hostname;
this.nodeurl = 'http://'+this.myip+':3000/api';
this.getdata();
document.body.className = 'single';
this.$root.headerHide = true;
this.$root.footerHide = true;
document.addEventListener('visibilitychange', this.visibilityChange, false); // 表示・非表示のイベント処理用
this.vhidden = [false, 0];
},
methods: {
:
:
},
watch: {
remaining: {
handler(value) {
if (value == 'dlna') {
this.getinfo();
}else if (value > 0 && this.playing && !this.vhidden[0]) { // 曲の再生中はcount down timer
setTimeout(() => {
this.remaining--;
}, 1000);
}else if (value == 0 && this.playing) { // play中なら次の曲情報を得る
this.getinfo();
}else if (this.vhidden[0]) { // 画面が非表示なら待つ
console.log('wait timer');
}else{ // stopが押されて再生中止
this.remaining = 0;
}
},
immediate: true // This ensures the watcher is triggered upon creation
}
}
}
</script>
●backend node
backend nodejsの窓口の抜粋 (2箇所、開発用と本番用で変える必要あり)
const express = require('express');
const app = express()
const router = express.Router();
const { PythonShell } = require('python-shell');
const fs = require('fs');
const readline = require("readline");
const os = require('os');
const path = require('path');
const port = process.env.PORT || 3000
const parse = require('csv-parse/sync');
app.use(express.json()); // for parsing application/json
app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
// /imgのパスを指定する(指定しないとimgアクセス時にCORBエラーになる)
app.use('/img', express.static(path.join(__dirname,'./img')));
var networkInterfaces = os.networkInterfaces();
var arr = networkInterfaces['wlan0'] // wire-less lan
var ip = arr[0].address;
// local使用なのでCORSポリシーを無効にしている。
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*"); // 全てのurlを許す
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
:
:
app.get('/api', function (req, res) { ← ※ ページ表示時の初期値を渡す
// clear
sel = '';
info = '';
script = '';
remaining = 0;
dlnaSrv = '';
musicSrc = baseFolders;
if(req.query.dat == 'radio') {
songs = radioList;
art = 'http://'+os.hostname()+'/~pi/music/img/radio.png';
}else if(req.query.dat == 'mfile' || req.query.dat == 'utube') {
songs = req.query.dat == 'mfile' ? musicSrc :utubeList;
art = 'http://'+os.hostname()+':3000/img/no-cover-art.jpg';
}
:
:
}else{ // dlna ← 250行目近辺
// for 開発用
// var dpyshell = new PythonShell('bkend/dlna.py');
// for 本番用
var dpyshell = new PythonShell('./dlna.py');
:
:
app.post('/api', function (req, res) { ← ※ UI側の命令を音楽再生プログラムへ渡すIF
if('stop' in req.body){
todo = 'stop'; // todo = 'stop'
info = '';
disabled = 'disabled';
remaining = 0;
if(req.body.stop == 'radio'){
art = 'http://'+os.hostname()+'/~pi/music/img/radio.png';
}else if(req.body.stop == 'mfile' || req.body.stop == 'utube'){
art = 'http://'+os.hostname()+':3000/img/no-cover-art.jpg';
}
}else if('songlist' in req.body){ // for radio
:
:
var pyshell = new PythonShell('./control.py'); ← ※ pythonの中継プログラム
pyshell.send(todo); // pythonにリクエストを送る
//pythonコード実施後にpythonからjsにデータが「data」に格納され引き渡される。
pyshell.on('message', (data) => {
if(!isNaN(todo)){ // 選局
parseInfo(JSON.parse(data));
}else if(baseFolder != '' && todo.indexOf(baseFolder) == 0){ // music file 選曲
:
:
//PythonShellのインスタンスpyshellを作成する。 ← 450行目近辺
// for 開発用
// var pyshell = new PythonShell('bkend/control.py');
// for 本番用
var pyshell = new PythonShell('./control.py');
:
:
app.listen(port, () => {
console.log(`listening on *:${port}`);
})
●python プログラム
index.jsから受けたコマンドを解析してmpvに送る
#!/usr/bin/python3
#-*- encoding: utf-8 -*-
import os,sys
import subprocess as proc
import json
import glob
import re
import time
import csv
import pandas as pd
from shelper1cli import SocketHelper
import dlna # my program
import getart # my program
sys.path.append("/home/pi/music/dispatcher")
import smplmpvctl as mpv # my program
hostname = '%s.local' % os.uname()[1]
:
:
data = sys.stdin.readline() # Node.js PythonShellからの標準入力でデータを取得する
data = data.replace('\n','')
:
:
●開発環境の起動
開発環境の起動には"vue-node"ディレクトリで2つの端末を起動する
1.1つの端末でnode.js backendを起動
$ npm start
2.もう1つの端末でvue.jsを起動
$ npm run serve
3.ブラウザで
http://(host ip):8082/
●本番環境の起動
本番用node.jsをデーモン起動させるためにpm2と云う物を導入してからデプロイする
●pm2の導入
$ npm install -g pm2
●'bkend'ディレクトリ内にpm2用のconfigファイルを作成する
$ cd bkend
$ nano pm2.yml ← ファイル名は適当で良い
name: bkend # アプリ名
script: ./index.js # スクリプトファイルパス
watch: false # フォルダやサブフォルダ内ファイルの変更時にアプリが再読み込みされないようにする
log-date-format: "YYYY-MM-DD HH:mm Z" # ログに日付を追加
1.本番用vueファイルをbuildする
$ cd vue-node ← 開発環境
$ npm run build
2.作成された'music-nodevue'とnode.jsの'bkend'ディレクトリを本番環境へデプロイ
3.配置した'bkend'ディレクトリでpm2を使い永続化し自動起動させる
$ cd bkend
$ npm init -y ← node.js用基本モジュールを導入
entry point: (index.js)以外はdefaultでenter
・追加モジュール
$ npm install express
$ npm install python-shell
$ npm install csv-parse
$ pm2 start pm2.yml
$ pm2 startup
ここで、下記のような実行方法の指示が表示されるので、それを実行
$ sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u pi --hp /home/pi
$ pm2 save
稼働確認は
$ pm2 list
自動起動の解除
$ pm2 unstartup systemd
ここで、下記のような実行方法の指示が表示されるので、それを実行
$ sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 unstartup systemd -u pi --hp /home/pi
4.ブラウザよりapatch配下にデプロイした'music-nodevue'へアクセスする
●pm2でdebug
bkendのindex.jsにconsole.log()を設定すると
$ pm2 log bkend
log情報が表示され続け、console.log()で設定したものが表示される
終了はctrl+C
●