[GAS] Steamで遊んだ時間をGoogleカレンダーに記録する
最近、NeosVRやVRChatなどのVRゲームをする時間が増えてきた。 ここ数年、ライフログとして自動的に記録できるアクティビティはGoogleカレンダーに記録するようにしている。
……というわけでSteamで何時から何時まで何のゲームを遊んでいたのかが自動的に記録されるようにしてみた。 APIを叩いてGoogleカレンダーに転記するだけのプログラムはGASで書くのが楽。
しくみ
SteamではWeb APIが公開されている。
この中にGetPlayerSummariesというエンドポイントがある。
これはユーザーの情報を取得するエンドポイントだが、ゲームを遊んでいる間だけ
gameid
gameextrainfo
というフィールドが生えてくる。 このAPIを定期的に叩き、gameidとgameextrainfoが生えている間はゲームのプレイ中としてカレンダーのイベントを作成・延長していく。
※ちなみに何時から何時までゲームをした、という情報を取れるエンドポイントは見当たらなかった
実装
Steam API Keyの発行
探せばいっぱい記事があるので省略。
GAS (Google Apps Script) を書く
適当なGoogleスプレッドシートを作成し、スクリプトエディタを開いてGASを書く。 Googleスプレッドシートは、直近で記録したGoogleカレンダーのイベントIDとゲーム情報を一時保存するために利用する。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const CALENDAR_ID = "YOUR_CALENDAR_ID"; | |
const STEAM_ID = "YOUR_STEAM_ID"; | |
const STEAM_API_KEY = "YOUR_STEAM_API_KEY"; | |
function run() { | |
const api = new SteamApi(STEAM_API_KEY); | |
const json = api.getPlayerSummary(STEAM_ID); | |
const gameid = json["gameid"]; | |
const gameextrainfo = json["gameextrainfo"]; | |
main(gameid, gameextrainfo); | |
} | |
function main(gameid, title) { | |
const cacheRange = SpreadsheetApp.getActiveSheet().getRange(2,1,1,5); | |
const cache = new SheetRangeCacheService(cacheRange, new GameEventConverter()); | |
if (!gameid || !title) { | |
Logger.log("clear"); | |
cache.clear(); | |
return; | |
} | |
const calendarService = new CalendarService(CALENDAR_ID); | |
const current = cache.restore(); | |
if (current && current.gameid == gameid) { | |
Logger.log("update"); | |
current.endTime = new Date(); | |
calendarService.updateEventEndTime(current.calendarEventId, current.endTime); | |
cache.store(current); | |
} else { | |
Logger.log("new"); | |
const startTime = new Date(); | |
const endTime = new Date(); | |
const calendarEventId = calendarService.createEvent(title, startTime, endTime); | |
const data = new GameEvent(gameid, title, calendarEventId, startTime, endTime) | |
cache.store(data); | |
} | |
} | |
class SteamApi { | |
constructor(apiKey) { | |
this.apiKey = apiKey; | |
} | |
getPlayerSummary(steamId) { | |
const url = `http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=${this.apiKey}&steamids=${steamId}&format=json`; | |
const options = { | |
"muteHttpExceptions": true | |
} | |
const res = UrlFetchApp.fetch(url, options); | |
if (res.getResponseCode() === 200) { | |
const content = res.getContentText(); | |
const json = JSON.parse(content); | |
return json.response.players[0]; | |
} else { | |
console.error(`${res.getResponseCode()}: ${res.getContentText()}`); | |
return {}; | |
} | |
} | |
} | |
class GameEvent { | |
constructor(gameid, title, calendarEventId, startTime, endTime) { | |
this.gameid = gameid; | |
this.title = title; | |
this.calendarEventId = calendarEventId; | |
this.startTime = startTime; | |
this.endTime = endTime; | |
} | |
} | |
class GameEventConverter { | |
toArray(gameEvent) { | |
return [ | |
gameEvent.gameid, | |
gameEvent.title, | |
gameEvent.calendarEventId, | |
gameEvent.startTime, | |
gameEvent.endTime | |
]; | |
} | |
fromArray(arr) { | |
return new GameEvent(arr[0], arr[1], arr[2], arr[3], arr[4]); | |
} | |
} | |
class SheetRangeCacheService { | |
constructor(range, converter) { | |
this.range = range; | |
this.converter = converter; | |
} | |
store(obj) { | |
const data = this.converter.toArray(obj); | |
this.range.setValues([data]); | |
} | |
restore() { | |
const data = this.range.getValues()[0]; | |
return this.converter.fromArray(data); | |
} | |
clear() { | |
this.range.clearContent(); | |
} | |
} | |
class CalendarService { | |
constructor(calendarId) { | |
this.calendar = CalendarApp.getCalendarById(calendarId); | |
} | |
createEvent(title, startTime, endTime) { | |
const res = this.calendar.createEvent(title, startTime, endTime); | |
const eventId = res.getId(); | |
return eventId; | |
} | |
updateEventEndTime(eventId, endTime) { | |
const event = this.calendar.getEventById(eventId); | |
const startTime = event.getStartTime(); | |
event.setTime(startTime, endTime); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function test_api() { | |
const api = new SteamApi(STEAM_API_KEY); | |
const json = api.getPlayerSummary(STEAM_ID); | |
Logger.info(json); | |
} | |
function test_NeosVR() { | |
main("111", "NeosVR"); | |
} | |
function test_VRChat() { | |
main("222", "VRChat"); | |
} | |
function test_clear() { | |
main(); | |
} |
実行トリガーの設定
run
関数を定期実行。
実行結果
色は薄いですがこんな感じ。