07.Node.jsとVue.jsでmusic player作成

バックエンドにNode.jsを使い、フロントUIはVue.jsで作成してmpvで音楽再生させるプレーヤを作ってみた。
量が多いので以下では個人的な備忘録として肝となる部分のみの内容です。

    機能:
  1. 音楽ファイル再生
  2. youtubeの再生
  3. net radioの再生
  4. DLNAの再生

●ブラウザの操作

ブラウザ側はVue.jsで作成したUIでNode.jsサーバ側とやり取りする。主な画面操作は以下の通り。

§
NodeVue playerのブラウザ画面
§


01.音楽プレーヤーのインデックス画面

02.playerのradio画面

03.playerの音楽ファイル画面

04.playerのyoutube画面

05.playerのdlna画面

06.playerのalbum art拡大画面

●開発環境の構成

下図の太字をクリックすると内容(一部抜粋の簡易版)の項へskip

●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

【参考】
  1. 【2022年】Raspberry Piに最新版の Node.jsを超簡単にインストールする方法
  2. Vue CLI installation
  3. Vue-CliとPythonの連携
  4. nodebrewでインストールしたpm2をRasbperry Piで自動起動させる
  5. PM2でnodejsアプリを動かす
  6. CSS3でPhotoshopで作ったようなキレイなガラス風ボタンを作るチュートリアル
  7. ツリービューをリストから作成できるjQueryプラグイン、”jsTree”
  8. HTML data : href on each JSTree node

作成:2023/07/02