はじめに
朝は、一日のはじまりなので、良いスタートを切りたい。
そんなとき、心地よい音楽を聴いて、気分を整えたいと思います。
LINEは、起き掛けにメッセージが入っていないか見るので、
おススメの音楽が届いていたら、うれしい気がします。
完成イメージ
環境
- Node.js
- LINE Messaging API
- Spotify SDK SpotifiApi
- VSCode
- Github Actions
作り方
- LINE Messaging APIの利用登録とチャネル作成*
- LINEログイン APIの利用登録とLIFFアプリ作成*
- Spotify APIの利用登録とWebアプリ作成*
- Github Actionsの設定*
*ほかのQiita記事に掲載あり
ソースコード
3. Webアプリ
Webアプリについて紹介します。
サーバ
/**
* 今朝のおススメの曲
*/
// ライブラリ
const express = require('./node_modules/express');
// Spotify用設定
const spotifyApp = express();
spotifyApp.use(express.json());
spotifyApp.use(express.urlencoded({ extended: true }));
const spotifyRouter = require('./spotify-router');
spotifyApp.use('/', spotifyRouter);
// LINE用設定
const lineApp = express();
const lineRouter = require('./line-router');
lineApp.use('/line', lineRouter);
// Spotify用ポート
spotifyApp.listen(8888, () =>
console.log(
'HTTP Server up. Now go to http://localhost:8888/login in your browser.'
)
);
// LINE用ポート
lineApp.listen(8000, () =>
console.log(
'HTTP Server up. Now go to http://localhost:8000/line/login in your browser.'
)
);
Line用ルータ
/**
* LINE用ルータ
*/
// ライブラリ
const express = require('./node_modules/express/index');
const line = require('@line/bot-sdk');
const axios = require('./node_modules/axios');
var util = require('./node_modules/util');
// LINE用設定
const app = express.Router();
const config = {
channelSecret: '<シークレットキー>',
channelAccessToken: '<アクセストークン>'
};
const client = new line.Client(config);
function makeFlexMessage(data) {
var flexMessage = {
type: "flex",
altText: "this",
contents: {}
};
var flexCarousel = {
type: "carousel",
contents: []
};
flexCarousel.contents = makeBubbles(data);
flexMessage.contents = flexCarousel;
return flexMessage;
}
function makeBubbles(data) {
var flexBubbles = [];
for(let i = 0; i < data.length; i++) {
if (i > 2) break;
flexBubbles.push(makeBubble(data[i]));
}
return flexBubbles;
}
function makeBubble(data) {
var flexBubble = {
type: "bubble",
size: "",
direction: "",
hero: {},
body: {},
footer: {}
};
flexBubble.size = "micro";
flexBubble.direction = "ltr";
flexBubble.hero = makeHero(data); // FlexImage;
flexBubble.body = makeBody(data); // FlexBox;
flexBubble.footer = makeFooter(data); // FlexBox;
return flexBubble;
}
function makeHero(data) {
var flexImage = {
type: "image",
url: "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip10.jpg",
size: "full",
aspectRatio: "320:213",
aspectMode: "cover"
};
return flexImage;
}
function makeBody(data) {
var flexBox = {
type: "box",
layout: "vertical",
contents: [],
spacing: "sm",
paddingAll: "13px"
};
flexBox.contents.push(makeBodyText(data)); //FlexComponent[]
flexBox.contents.push(makeBodyHeader(data));
flexBox.contents.push(makeBodyMain(data));
return flexBox;
}
function makeBodyText(data) {
var flexText = {
type: "text",
text: data.name,
size: "sm",
wrap: true,
weight: "bold",
style: "normal",
};
return flexText;
}
function makeBodyHeader(data) {
var flexBox = {
type: "box",
layout: "baseline",
contents: []
};
var stars = Math.round(data.popularity/10);
if (stars > 5) {
stars = 5;
}
for(let i = 0; i < stars; i++) {
flexBox.contents.push(makeBodyHeaderEvaluationIcon(data)); //FlexComponent[];
}
flexBox.contents.push(makeBodyHeaderEvaluationNumber(data)); //FlexComponent[];
return flexBox;
}
function makeBodyHeaderEvaluationIcon(data) {
var flexIcon = {
type: "icon",
url: "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
size: "xs"
};
return flexIcon;
}
function makeBodyHeaderEvaluationNumber(data) {
var flexText = {
type: "text",
text: "",
size: "xs",
color: "#8c8c8c",
margin: "md",
flex: 0
};
var value = data.popularity;
if (value === null || value === undefined) {
value = " ";
}
flexText.text = "'" + value + "'";
return flexText;
}
function makeBodyMain(data) {
var flexBox = {
type: "box",
layout: "vertical",
contents: []
};
flexBox.contents.push(makeBodyMainContext(data)); //FlexComponent[];
return flexBox;
}
function makeBodyMainContext(data) {
var flexBox = {
type: "box",
layout: "baseline",
spacing: "sm",
contents: []
};
var flexText = {
type: "text",
text: data.artist,
size: "xs",
wrap: true,
color: "#8c8c8c",
flex: 5
};
flexBox.contents.push(flexText);
return flexBox;
}
function makeFooter(data) {
var flexBox = {
type: "box",
layout: "vertical",
spacing: "sm",
contents: []
};
var flexButton = {
type: "button",
style: "primary",
color: "#228b22",
action: ""
};
var action = {};
action.label = "Play";
action.type = "uri";
action.uri = data.url;
flexButton.action = action;
flexBox.contents.push(flexButton);
return flexBox;
}
async function handleEvent(event) {
if (event.type !== 'message' || event.message.type !== 'text') {
return Promise.resolve(null);
}
var msg = '今朝のおススメの曲';
let url = "http://localhost:8888/getmusic";
let response = await axios.get(url);
var flexMessage = makeFlexMessage(response.data);
console.log(util.inspect(flexMessage));
// ユーザーにリプライメッセージを送ります。
return client.replyMessage(event.replyToken, flexMessage);
}
//=================
// ルータ
//=================
app.get('/', (req, res) => res.send('Hello LINE BOT! (HTTP GET)'));
app.post('/webhook', line.middleware(config), (req, res) => {
if (req.body.events.length === 0) {
res.send('Hello LINE BOT! (HTTP POST)');
console.log('検証イベントを受信しました!');
return;
} else {
console.log('受信しました:', req.body.events);
}
Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});
module.exports = app;
Spotify用ルータ
/**
* Spotify用ルータ
*/
// ライブラリ
const ServerMethods = require('./src/server-methods');
const SpotifyWebApi = require('./src/spotify-web-api');
const express = require('./node_modules/express');
var util = require('./node_modules/util');
// Spotify用設定
const app = express.Router();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 操作 ※ただし、Primeプラン限定あり
const scopes = [
'ugc-image-upload',
'user-read-playback-state',
'user-modify-playback-state',
'user-read-currently-playing',
'streaming',
'app-remote-control',
'user-read-email',
'user-read-private',
'playlist-read-collaborative',
'playlist-modify-public',
'playlist-read-private',
'playlist-modify-private',
'user-library-modify',
'user-library-read',
'user-top-read',
'user-read-playback-position',
'user-read-recently-played',
'user-follow-read',
'user-follow-modify'
];
// Spotify API SDK 設定
SpotifyWebApi._addMethods(ServerMethods);
const spotifyApi = new SpotifyWebApi({
redirectUri: 'http://localhost:8888/callback',
clientId: process.argv.slice(2)[0], // <クライアントID>
clientSecret: process.argv.slice(2)[1] // <クライアントシークレット>
});
//=================
// ルータ
//=================
// ログイン
app.get('/login', (req, res) => {
res.redirect(spotifyApi.createAuthorizeURL(scopes));
});
// Spotify からのコールバック
app.get('/callback', (req, res) => {
const error = req.query.error;
const code = req.query.code;
const state = req.query.state;
if (error) {
console.error('Callback Error:', error);
res.send(`Callback Error: ${error}`);
return;
}
// トークン取得
spotifyApi
.authorizationCodeGrant(code)
.then(data => {
const access_token = data.body['access_token'];
const refresh_token = data.body['refresh_token'];
const expires_in = data.body['expires_in'];
spotifyApi.setAccessToken(access_token);
spotifyApi.setRefreshToken(refresh_token);
console.log('access_token:', access_token);
console.log('refresh_token:', refresh_token);
console.log(
`Sucessfully retreived access token. Expires in ${expires_in} s.`
);
res.send('Success! You can now close the window.');
// トークン・リフレッシュ※期限の半分を過ぎたら
setInterval(async () => {
const data = await spotifyApi.refreshAccessToken();
const access_token = data.body['access_token'];
console.log('The access token has been refreshed!');
console.log('access_token:', access_token);
spotifyApi.setAccessToken(access_token);
}, expires_in / 2 * 1000);
})
.catch(error => {
console.error('Error getting Tokens:', error);
res.send(`Error getting Tokens: ${error}`);
});
});
// 今朝のおススメ曲取得※20曲
app.get('/getmusic', (req, res) => {
spotifyApi
.searchTracks('Good Morning')
.then(function(data) {
// Print some information about the results
console.log('I got ' + data.body.tracks.total + ' results!');
// Go through the first page of results
var firstPage = data.body.tracks.items;
console.log('The tracks in the first page are (popularity in parentheses):');
let results = [];
firstPage.forEach(function(track, index) {
results.push({'index' :index
, 'name' :track.name
, 'duration' :Math.round(track.duration_ms/60000)
, 'popularity' :track.popularity
, 'url' :track.external_urls.spotify
, 'preview_url' :track.preview_url
, 'artist' :track.artists[0].name
});
});
results.sort(function(first, second){
return second.popularity - first.popularity;
});
console.log(util.inspect(results));
return res.json(results);
}).catch(function(err) {
console.log('Something went wrong:', err.message);
});
});
module.exports = app;
おわりに
LINE公式アプリを見ると、FlexMessage や メニュー を上手く使用して UX を高めているのがわかります。
見習いたいですね。
デバッグについて書き添えます。
FlexMessage は、
1. LINE FlexMessage Simulator で作成して、コーディングして、
2. コンソール等のログにメッセージのJSONを出力して、
3. それを LINE FlexMessage Simulator で正しく表示されるかを確認するとよいと思います。
コードだけだとエラーがわかりにくいです。