GoogleカレンダーからObsidianのDailyノートへの予定自動取り込み備忘録

<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google%A5%AB%A5%EC%A5%F3%A5%C0%A1%BC">Googleカレンダー</a> → Obsidian(Dailyノート)自動取り込み|実装メモ

今年のお盆休みはどこにも行かず、家でひたすら慣れないツール作りに励んでいた。ChatGPTに相談しつつ、失敗を繰り返しながらの作業。時折見せるChatGPTのポンコツっぷりにイラつくも、私のプロンプトが悪いのは明白。アレコレうるさいオッサンを宥めるのに、相当苦労したんじゃないかな。ごめんな、GPT。

今回作りたかったのは、ChatGPTからGoogleカレンダーに予定を入力するツールと、Googleカレンダーに登録された予定をObsidianのDailyノートに自動で追加するツール。結局前者は失敗し、今もそのプロジェクトは続いている。後者は成功し、安定して動いている。以下、どのような構成で、どのような苦労があったかを備忘録的にまとめておく。


1. 目的と要件

  • 入力(入口)は GoogleカレンダーiPhone/職場Mac/自宅Mac から共通で入れやすい。
  • 記録(出口)は ObsidianのDailyノート:当日の出来事ログを自動追記。
  • 終日と時間ありを区別。時間ありは HH:MM-HH:MM 表記、終日は (終日) を付与。
  • Zapierは「1イベント=1 Markdownファイル」をGoogle Driveに作成(Appendが見つからなかった)。Mac側で取り込み後に archive/ へ移動。

2. 全体構成

  • ZapierGoogle CalendarGoogle Drive(1イベント→1つの.md
  • macOS LaunchAgentGoogle Drive の受け皿フォルダを監視し、変化で Python 起動
  • PythonMarkdownから「タイトル」「開始・終了(または終日)」を抽出し、Obsidianの Daily/YYYY/MM/YYYY-MM-DD.md に追記 → 処理済みの.mdファイルは archive/ 移動
※ 本文のパスやアカウント名は匿名化しています。例:ossan@example.com/Users/ossan/...

3. Zapier(GoogleカレンダーGoogle Drive

3.1 トリガ

  • Google CalendarNew or Updated Eventでテスト。
  • 取得フィールド例:Summary(件名)、Start Date TimeEnd Date TimeAll-day

3.2 変換(任意)

  • Formatter(Date/Time):Zapier側の選択肢で、ISOのままでも後段Pythonで整形可能。

3.3 出力

  • Google DriveUpload/Create File(1イベント=1ファイル)。
  • ファイル名例:2025-08-17_頭を剃る.md
  • 本文例(Zapierのテンプレ内):
     - [ ] 頭を剃る 2025-08-17T17:00:00+09:00から2025-08-17T18:00:00+09:00
    

4. フォルダ監視(launchd, LaunchAgent)

ユーザセッションで動かすため ~/Library/LaunchAgents/plist を配置。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key><string>com.ossan.pullgcal.watch</string>

    <key>ProgramArguments</key>
    <array>
      <string>/Library/Frameworks/Python.framework/Versions/3.13/bin/python3</string>
      <string>/Users/ossan/bin/pull_gcal_feed.py</string>
    </array>

    <key>WorkingDirectory</key>
    <string>/Users/ossan/bin</string>

    <key>StandardOutPath</key><string>/tmp/pull_gcal.out</string>
    <key>StandardErrorPath</key><string>/tmp/pull_gcal.err</string>

    <key>WatchPaths</key>
    <array>
      <string>/Users/ossan/Library/CloudStorage/GoogleDrive-foo@example.com/マイドライブ/ZapierCalendarFeed</string>
    </array>

    <key>RunAtLoad</key><true/>
    <key>ThrottleInterval</key><integer>10</integer>
  </dict>
</plist>

有効化:launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ossan.pullgcal.watch.plist 更新時:launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.ossan.pullgcal.watch.plistbootstrap 状態確認:launchctl print gui/$(id -u)/com.ossan.pullgcal.watch


5. Python(抽出・整形・追記・アーカイブ

5.1 入口・出口のパス

from pathlib import Path
import re, shutil
from datetime import datetime
from zoneinfo import ZoneInfo

TZ = ZoneInfo("Asia/Tokyo")

INBOX  = Path("/Users/ossan/Library/CloudStorage/GoogleDrive-foo@example.com/マイドライブ/ZapierCalendarFeed")
ARCH   = INBOX / "archive"
DAILY  = Path("/Volumes/knowledge/Obsidian/2nd brain/daily")

5.2 イベント行の抽出(時間あり/終日)

ISO_RANGE_RE = re.compile(
    r"^- \[\s?\]\s*(?P<title>.*?)\s+"
    r"(?P<s>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:[+\-]\d{2}:\d{2})?)から"
    r"(?P<e>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:[+\-]\d{2}:\d{2})?)$"
)
ALLDAY_RE = re.compile(
    r"^- \[\s?\]\s*(?P<title>.*?)\s+(?P<d>\d{4}-\d{2}-\d{2})$"
)

def parse_dt(s: str) -> datetime:
    # 例: 2025-08-17T17:00:00+09:00
    return datetime.fromisoformat(s)

5.3 ノーマライズ(表示文言の生成)

def normalize_line(raw: str):
    m = ISO_RANGE_RE.search(raw)
    if m:
        title = m.group("title").strip()
        s = parse_dt(m.group("s")).astimezone(TZ)
        e = parse_dt(m.group("e")).astimezone(TZ)
        day = s.strftime("%Y-%m-%d")
        text = f"- [ ] {title} {s:%H:%M}-{e:%H:%M}"
        return day, text

    m2 = ALLDAY_RE.search(raw)
    if m2:
        title = m2.group("title").strip()
        day = m2.group("d")
        text = f"- [ ] {title} (終日)"
        return day, text

    return None, None

5.4 Dailyノートに追記(自動で年月フォルダを作成)

def append_to_daily(day: str, line: str):
    yyyy = day[:4]
    mm   = day[5:7]
    dest_dir = DAILY / yyyy / mm
    dest_dir.mkdir(parents=True, exist_ok=True)
    dest = dest_dir / f"{day}.md"
    with dest.open("a", encoding="utf-8") as f:
        f.write(line + "\n")
    return dest

5.5 受け皿の .md を取り込み → archive へ移動

def process_inbox():
    ARCH.mkdir(exist_ok=True)
    processed = 0
    for p in sorted(INBOX.glob("*.md")):
        try:
            raw = p.read_text(encoding="utf-8").strip()
            day, line = normalize_line(raw)
            if day and line:
                dest = append_to_daily(day, line)
                print(f"[OK] {p.name} → {dest}")
                shutil.move(str(p), str(ARCH / p.name))
                processed += 1
            else:
                print(f"[SKIP] {p.name}(フォーマット不一致)")
        except Exception as e:
            print(f"[ERR] {p.name}: {e}")
    print(f"[DONE] processed={processed}")

LaunchAgent からは pull_gcal_feed.pyif __name__ == "__main__": process_inbox() を叩くだけ。


6. 運用とログ

  • 受け皿にファイルが生成されると、数秒〜十数秒で取り込み → Daily追記 → archive/ 移動。
  • ログ:/tmp/pull_gcal.out(正常系・進捗)、/tmp/pull_gcal.err(例外など)。
  • 重複はZapier側で同一イベントの多重発火が無ければ概ね起きない。起きた場合は受け皿に来るファイルが複製されていないか確認。

7. 失敗談:Timebox直書き同期の壁

Obsidianの timebox.md#timeboxstart/due を記入 → PythonGoogle Calendar API へ同期するアプローチも試した。重複回避に extendedProperties.private.fingerprint を持たせてハッシュ管理したが、次の点で運用が重くなった。

  • 複数端末・iPhoneからの入力は結局「googleカレンダー」のほうが速い(笑)
  • Obsidian→Google の「片側主従」は破綻しやすい(現場で予定を変える起点がGoogle側にある)。

結論として、「入口=Googleカレンダー、記録=Obsidian」に役割分担したほうがシンプルで安定した。


8. 技術的な課題と対処

  • ZapierのAppend不在:1イベント=1ファイルを生成し、Mac側で吸収・追記・アーカイブする方式で解決。
  • Google Driveの同期遅延:ストリーミング(ファイルオンデマンド)でも数秒の遅延は起こり得る。LaunchAgentの ThrottleInterval を短くしすぎると無駄起動が増えるため注意する。
  • macOS権限:フルディスクアクセス・ログイン時起動の権限漏れで失敗しやすい。LaunchAgentのログをまず確認。

9. 未達成:ChatGPT → Googleカレンダー直接登録

「自然文(例:『来週水曜の午後に30分、仕事の計画』)→ 予定作成」を目指しているが、API連携の方式・権限・審査など現時点の自分の環境では未整備。ChatGPT側の問題もあるのではないかと考察しているが、詳細は不明。そのうちに正式に対応してくれるのを待つのもいいかも。


10. まとめ

  • 入力はGoogleカレンダー、記録はObsidianに集約する設計が現実的。
  • Zapierで「1イベント=1ファイル」→ LaunchAgent監視 → Pythonで整形・追記・アーカイブ、の三段構えで安定。
  • 相当疲れた。Zapierは相当便利だが、慣れるまでちょっと戸惑った。
  • ChatGPTは素晴らしいが、プロンプトを打つオッサンがヘボいと、余計な回り道を強いられる。プロンプトの研究が必要。