本記事は、株式会社ピー・アール・オーアドベントカレンダーの5日目です。
実際に作っていきましょう
前回は、やりたいことと方針のみグダグダと書いてしまいましたが。今回からはいよいよ実際の機能を作っていきたいと思います。
今回のゴール
Garmin Connectから取得可能なデータのうち、日々の活動記録の方をChrome拡張からダウンロードできるようにしてみます。なぜ日々の活動記録を選んだかというと、こっちの方がURLに日付が含まれるため、自動でURLを作るのが楽だからです。もう一方のアクティビティはアクティビティID取得したりしないといけないんで、そっちは追々追加します。
そしてダウンロードする際の要件はなんとなく以下で考えてみました。
- 拡張機能独自のメニューを持つ
- メニューでは以下を設定可能にする
- ダウンロードON/OFF
- 日時(いつからのデータをダウンロードするか?)
- ダウンロード場所を指定させる
- ダウンロード中もブラウザで他の作業が可能なようにする(バックグラウンドでDLは行う)
- サーバ負荷を避けるために、一定間隔でのDLを行う
- いつまでDLしたかを記録しておき、途中で中断してもそこから再開できる
何はなくともChrome拡張のひな型を作る
Chrome拡張、何気に作るのは初めてです。
例によって例のごとく、Qiitaで参考になりそうな記事を探しながら進めていきます。
タグ:chrome-extensionで690記事(2020/12/4時点)もあるので選り取り見取りといったところです。
manifest.json
とりあえず、一番最初は以下のようなシンプルなものにしてみました。
{
"name": "Garmin Connect Downloader",
"version": "1.0",
"description": "Download all activities and healthcare original data",
"permissions": ["declarativeContent", "storage"],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"page_action": {
"default_popup": "popup.html",
"default_icon": {
"32": "images/get_started32.png"
}
},
"manifest_version": 2
}
permissionsは、とりあえずgarmin connectのページでのみ本拡張機能を有効にしようかと思いdeclarativeContentを、設定値の保存用にstorageをチョイスしてみました。
あと、background.jsはバックグラウンドでのDL処理を行う場として、
page_actionは拡張機能のメニューとして定義しています。
popup.html
popup.htmlに、あらかじめ設定として必要なものをUIとして入れておきます。
コメントアウトしている個所は今回は実装しない予定のものです。
<!DOCTYPE html>
<html>
<head>
<script src="lib/jquery-3.5.1.min.js"></script>
<link href="styles/datepicker.css" rel="stylesheet">
<link href="styles/popup.css" rel="stylesheet">
<script src="lib/datepicker.js"></script>
</head>
<body>
<div class="whole-content">
<!--<button id="changeColor"></button>-->
<h2>Garmin Connect Downloader</h2>
<div>
<div class="item">
<span>Download after this date</span>
<input type="text" class="startDate" data-toggle="datepicker">
</div>
<!--
<div class="item">
<div>Download data</div>
<label>Activity</label><input type="checkbox" class="checkActivity">
<label>Healthcare</label><input type="checkbox" class="checkHealthcare">
</div>
</div>
-->
<!--
<div class="item">
<span>Download interval (seconds)</span>
<input type="text" class="intervalTime" value="1">
</div>
-->
<div class="item">
<span>Download Directory</span>
<input type="text" class="dlDirectory">
</div>
<div class="item">
<span>Open Garmin Connect</span>
<div>
<a href="https://connect.garmin.com/modern/">Garmin Connect</a>
</div>
</div>
<div>
<p class="toggle-title">download</p>
<div class="toggle-switch">
<input id="toggle" class="toggle-input" type='checkbox' />
<label for="toggle" class="toggle-label">
<span></span>
</div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
なお、画面中DL状態を表すためにトグルスイッチを付けたくて、以下の記事を丸パクリ参考にさせていただいて実現しました。ありがとうございます。
CSSで作るToggle Switchを学んだのでさっそく作ってみた
一つ一つ作っていきます。
設定画面の実装
値の出し入れのところから作っていきましょう。
Chrome Extension APIの、chrome.storage.sync.setおよびchrome.storage.sync.getを使って実現できそうです。
// Save startDate to local storage
$('[data-toggle="datepicker"]').change(function(){
var date = $(this).val();
chrome.storage.sync.set({start_date: date}, function() {
console.log(date);
});
});
// Save directory to local storage
$('.dlDirectory').change(function(){
var directory = $(this).val();
chrome.storage.sync.set({directory: directory}, function() {
console.log(directory);
});
});
// Restore
chrome.storage.sync.get('start_date', function(data) {
$('[data-toggle="datepicker"]').val(data.start_date);
});
chrome.storage.sync.get('directory', function(data) {
$('.dlDirectory').val(data.directory);
});
こんな感じですね。
トグルスイッチ変更時のイベントハンドラ
イベントハンドラで処理のとっかかりを作ります。
// Toggle changed
$(".toggle-input").click(function toggleHandler(e) {
console.log('handle on');
var toggled = $(this).prop('checked');
// Set params to storage
chrome.storage.sync.set({isStart: toggled}, function() {
console.log('isStart ' + toggled);
});
});
// Restore
chrome.storage.sync.get('isStart', function(data) {
$('.toggle-input').prop('checked', data.isStart);
});
これで、設定画面から登録した値と、トグルスイッチのイベントを受け取れるようになりました。
カレンダコントロールの導入
日付入力があるので入れましょう(入れました)。
<!DOCTYPE html>
<html>
<head>
<script src="lib/jquery-3.5.1.min.js"></script>
<link href="styles/datepicker.css" rel="stylesheet">
<link href="styles/popup.css" rel="stylesheet">
<script src="lib/datepicker.js"></script>
</head>
// DatePicker
$('[data-toggle="datepicker"]').datepicker({
format: 'yyyy-mm-dd'
});
backgroundの実装
page側はほぼできたので、実際のダウンロード処理周りを作っていきます。
まず一番重要なのは、どうやってDLするか?です。単に指定のURLをwindow.openするだけでもDLはできましたが、一瞬ウインドウが開かれてしまうのがイケてない感じなので却下です。
調べてみたところ、やはりChrome Extension APIのchrome.downloads.downloadが使えそうでしたので調べてみましたところ、一つだけ残念な点が。。。
はい。完全パスはダメよって言われちゃってますね。'..'も封じられているのでディレクトリトラバース的なやり方も駄目のようです。任意ディレクトリを本拡張機能の中だけで指定することはできなそうです(APIマニュアル見る前から薄々気づいてはいました。そりゃそうだ)。
まあ仕方ないので、保存場所は相対パスでのみ選べるようにはしておきましょう。
次に、定期実行処理ですが、はじめはsetTimeOutでの実装を考えていたんですが、こちらも調べてみたところ便利そうなAPIがあったのでそっちを使うことにしました(chrome.alarms.onAlarm.addListener)。
定期実行とダウンロード処理を組み合わせたのが以下コードです。
/**
* Set alarm when installing extension
*/
chrome.runtime.onInstalled.addListener(function (details) {
console.log(details.reason);
chrome.alarms.create("dl_fire", { "periodInMinutes": 1 });
});
/**
* Set alarm for regular download in the background.
*/
chrome.alarms.onAlarm.addListener(async function (alarm) {
let _isStart = await getLocalStorageVal('isStart');
if (_isStart.isStart && alarm.name == "dl_fire") {
let _startDateString = await getLocalStorageVal('start_date');
var start = moment(_startDateString.start_date);
//Download until yesterday
var yesterday = moment().subtract(1, 'd');
if(yesterday.isAfter(start)) {
var urlDate = start.format('YYYY-MM-DD');
var url = 'https://connect.garmin.com/modern/proxy/download-service/files/wellness/' + urlDate;
var _dir = await getLocalStorageVal('directory');
chrome.downloads.download({
url: url,
filename: _dir.directory + urlDate + '.zip'
});
start = start.add(1, 'd');
chrome.storage.sync.set({start_date: start.format('YYYY-MM-DD')}, function() {
// Save next date to local storage
});
}
}
});
アラームのセットをchrome.runtime.onInstalledでやってますが、これはこのタイミングで実行しないとアラームがうまく発火してくれなかったためです。本当にそうなのか?はまだ調べてないのでわかりません。
本当はDL間隔も設定画面で任意に変更したいので、こちらの問題についてはその時が来たら取り雲くことになるでしょう(なので今は塩漬け)。
ダウンロードしたら、次の取得日時をchrome.storage.sync.setでローカルストレージを更新するようにしました。お手軽対応ですが、一応一旦停止した後後日再取得もできるようにしています。
また、上記コードの中で呼び出しているgetLocalStorageValというかっちょ悪い名前のメソッドは、ローカルストレージからの値読み出しでawaitさせるためだけのメソッドです。
awaitさせないで書こうとすると当然コールバック地獄が待ってますので、いわゆる必要悪というやつです(違う)。
/**
* Get from local storage
* @param {String} key
*/
async function getLocalStorageVal(key) {
return new Promise((resolve, reject) => {
try {
chrome.storage.sync.get(key, function(value) {
resolve(value);
})
} catch (ex) {
reject(ex);
}
});
}
そうそう。chrome.alarms.onAlarmとchrome.downloads.downloadの利用にあたってはmanifestの"permission"に追加が必要でした。あとついでに、javascriptでの日付演算なんてかったるくてやりたくなかったのでmoment.jsも組み込みました。なのでmanifest.jsonは以下のようになりました。
{
"name": "Garmin Connect Downloader",
"version": "1.0",
"description": "Download all activities and healthcare original data",
"permissions": ["activeTab","declarativeContent", "storage", "downloads", "alarms"],
"background": {
"scripts": ["lib/moment.min.js", "background.js"],
"persistent": true
},
"page_action": {
"default_popup": "popup.html",
"default_icon": {
"32": "images/get_started32.png"
}
},
"icons": {
"48": "images/get_started48.png"
},
"options_page":"option.html",
"manifest_version": 2
}
さあこれで最低限の実装ができました。
実際にChromeに組み込んで動かしてみます。
chrome://extensionsを開き、「パッケージ化されていない拡張機能を読み込み」からソースディレクトリを指定します。
ちなみに、ここで「バッググランドページ」を開くとbackground.jsで出したconsole.logなどが見れるようになります(私は途中で気づきました)。たぶん・・・常識ですね。
拡張機能の設定画面はこんな感じ。まあ気の抜けた感じです。トグルスイッチだけ完成度高くて逆に変という・・・
アラームで設定した間隔(今回は1分)で、Garmin Connectからダウンロード開始されます。
バックグラウンドなのでQiitaを書きながらでも大丈夫w
まとめ
とりあえずChrome拡張初心者が1パス通すところまではできました。
これを励みに残りの機能も作っていきたいと思います。
(あ、この一連のシリーズは弊社のAdvent Calendarで誰も記事をエントリしてない日用のピンチヒッターとして用意したものですので、他に書く人がいた場合は永遠に続きがないかもしれません。あと筆者が飽きた時なども同様)