試してみたブログ

AI関連・iPhone/Pixelなどのガジェット・音声入力・サーマルプリンタなど興味をある事をどんどん試してみた際の記録

毎日の記録をショートカットアプリでGoogleカレンダーに登録してPixelaで可視化する

背景

  • 2024年から週4〜5でボルダリングジムに通っている
  • 手軽に行った日を記録したい
  • 年末には可視化したい

達成するには

  • iPhoneで手軽に記録出来る様にする
    • ショートカットアプリを押すと記録できるぐらいの手軽さ
    • 専用のカレンダーに転記されるのが良い
  • 月何回いけたかをカウント出来る様にする(Pythonで集計)
  • 年末にはスプシに出力したい
  • Pixelaを使って草を生やしてみる

試してみた

iPhoneで手軽に記録する

  • 専用のGoogleカレンダーを作成する

  • 上記の様に行ったボルダリングジムの名前が自動的に入る様にする

  • iPhoneのショートカットアプリで、カレンダーアプリ、指定のボルダリングカレンダーに終日で登録させる

  • これでワンタップで指定カレンダーに特定文言を持たせて登録前まで出来る(付け忘れの際には日付けを変更すればOK)

月次でカウントする

  • Pythonでサクッと集計します
import sys
from datetime import datetime, timedelta
from googleapiclient.discovery import build
from google.oauth2 import service_account
import calendar

SERVICE_ACCOUNT_FILE = 'credentials.json'
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
CALENDAR_ID = 'XXXXX'

def get_month_range(year: int, month: int):
    start = datetime(year, month, 1, 0, 0, 0)
    last_day = calendar.monthrange(year, month)[1]
    end = datetime(year, month, last_day, 23, 59, 59)
    return start, end

def main():
    args = sys.argv[1:]
    now = datetime.now()

    if not args:
        # デフォルトは今月
        target_year = now.year
        target_month = now.month
    elif args[0] == "prev":
        # 「prev」指定で前月
        prev_month_last_day = now.replace(day=1) - timedelta(days=1)
        target_year = prev_month_last_day.year
        target_month = prev_month_last_day.month
    else:
        # YYYY MM 形式で指定
        try:
            target_year = int(args[0])
            target_month = int(args[1])
        except (ValueError, IndexError):
            print("使い方: python count_events.py [prev] または [YYYY MM]")
            sys.exit(1)

    start_dt, end_dt = get_month_range(target_year, target_month)

    # Google API認証
    creds = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE, scopes=SCOPES
    )
    service = build('calendar', 'v3', credentials=creds)

    events_result = service.events().list(
        calendarId=CALENDAR_ID,
        timeMin=start_dt.isoformat() + 'Z',
        timeMax=end_dt.isoformat() + 'Z',
        singleEvents=True
    ).execute()
    events = events_result.get('items', [])

    print(f"{target_year}年{target_month}月の件数: {len(events)}")

if __name__ == "__main__":
    main()

  • 将来的にはLambdaなどに移植してここもiPhoneからサクッと集計できるようにしたいところ

スプシに転記する

  • 専用のGoogleカレンダーに記録しているので、上記の様に件数取得も簡単だし、二次利用も可能です
  • 今回は可視化の前段階でいったんデータを引っこ抜きます
function onOpen() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();

  var subMenus = [
    { name: "ボルダリング予定を取得", functionName: "exportBoulderingEvents" }
  ];

  ss.addMenu("カレンダー連携", subMenus);
}

function exportBoulderingEvents() {
  // ★ボルダリング用カレンダーID
  const CAL_ID = "XXXXXX";

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();

  // 期間はシートから取得(例:B1=開始日, B2=終了日)
  const startTime = sheet.getRange("B1").getValue();  // Date型
  const endTime   = sheet.getRange("B2").getValue();  // Date型

  if (!(startTime instanceof Date) || !(endTime instanceof Date)) {
    Browser.msgBox("B1, B2 に有効な日付を入力してください。");
    return;
  }

  // カレンダー取得
  const calendar = CalendarApp.getCalendarById(CAL_ID);

  // 期間内のイベントを取得
  const events = calendar.getEvents(startTime, endTime); // [web:13][web:18]

  // 見出し行の下から書き込むので、一旦既存データを削除(任意)
  const lastRow = sheet.getLastRow();
  if (lastRow > 1) {
    sheet.getRange(2, 1, lastRow - 1, 2).clearContent();
  }

  if (events.length === 0) {
    Browser.msgBox("指定期間内にイベントがありません。");
    return;
  }

  // 2次元配列を作成して一括書き込み
  const values = events.map(function(event) {
    const start = event.getStartTime(); // Date [web:19]
    const dateStr = Utilities.formatDate(start, Session.getScriptTimeZone(), "yyyy/MM/dd");
    const title = event.getTitle();     // [web:13]
    return [dateStr, title];
  });

  // A列:日付, B列:タイトルとして書き出し
  sheet.getRange(2, 1, values.length, 2).setValues(values);

  Browser.msgBox("ボルダリング予定の取得が完了しました。");
}
  • キレイにデータが抜けました

Pixelaで可視化する

  • 今回の可視化にはPixelaを使用します
  • Githubの柴の様に草で可視化が出来、またAPIでサクッと作れるのでオススメです
  • スプシから式でCurlを組み立てて行きます

  • 上記で作成したCurlを1つのシェルファイルにまとめて、$sh bold.sh の様な形で実行します

  • 注意点としては無料プランだとAPI使用時に25%の割合で失敗する仕様になっているので、年末だけでも是非Patreonとして支援しましょう

振り返り

1年間のボルダリングジム通いが可視化されました!!キレイな柴に育ってくれてうれしいです!

また記録を付ける+Pixelaで可視化していく事自体とても楽しく、来年も頑張ろうという気持ちになれました。

今後はiPhoneのカレンダーショートカットの中に、Pixelaへの投稿を組み込んだり、月次集計のショートカットを組み込んだりを試して行きたいと思います!