整理公司或個人行程時,你可能早就不只一個行事曆:團隊共享、外部訂閱、臨時專案… 全都堆在 Google 日曆裡。
與其逐一點開確認,不如一次把名單抓出來。這篇會用 Apps Script 的 CalendarApp.getAllCalendars(),把「我擁有或已訂閱」的行事曆通通列出,順便寫進試算表做索引。你會看到最小可行範例、常見參數、權限授權注意事項,也會示範怎麼標記主要行事曆、分辨是否有寫入權、找出被隱藏但其實很重要的日曆。
最後會整理常見雷點與錯誤排查,幫你少走彎路。讀完你就能把零散的行程資產,變成可搜尋、可維護、可擴充的資訊清單。希望本篇的文章可以幫助到需要的您。
目錄
{tocify} $title={目錄}
為什麼需要 getAllCalendars()?
在自動化流程裡,我們常常不只操作主要行事曆,還會碰到團隊共用、訂閱的外部行事曆、或專案臨時建立的次要行事曆。
CalendarApp.getAllCalendars() 的用處就在於:一次拿到使用者帳號底下「擁有或已訂閱」的所有行事曆,再依名稱、ID、擁有權、是否隱藏等屬性做後續處理(同步事件、報表彙整、權限稽核、顏色規範等)。
依 Google 官方文件說明,這個方法會回傳一組 Calendar 物件陣列,涵蓋使用者擁有或已訂閱的行事曆。
核心觀念與前置條件
1. 需要的服務與權限(Scopes)
Apps Script 的 Calendar 服務可讀寫使用者行事曆,大多數方法(包含抓取所有行事曆)在第一次執行時會跳出授權,常見授權範圍包含 https://www.googleapis.com/auth/calendar 等。若沒有正確授權,存取將被拒絕。
2. 物件與常用屬性
CalendarApp.getAllCalendars() 回傳的是 Calendar[]。每個 Calendar 物件都能再呼叫屬性/方法,例如:
getId():取得行事曆 ID(做 API 互動、長期紀錄必備)
getName():顯示名稱
isOwnedByMe():判斷是否本人擁有
isMyPrimaryCalendar():是否為主要行事曆
isHidden() / isSelected():是否隱藏/是否在介面顯示
上述能力皆可從 CalendarApp/Calendar 類別文件查到對應方法。
3. 與 Calendar REST API 的關係
若你之後要串接純 REST(例如用 UrlFetchApp 打 API),取得「使用者清單上的行事曆」則對應 CalendarList.list 端點;若操作事件清單,會用到 events.list,其中主要行事曆可用字串 "primary" 代表。這些是與 Apps Script 內建服務相輔相成的知識點。
快速上手:最小可行範例
目標:列出目前帳號底下所有行事曆(擁有+訂閱),輸出名稱、ID、是否主要、是否本人擁有、是否隱藏。
function listAllCalendars() {
const cals = CalendarApp.getAllCalendars(); // 取得所有擁有或已訂閱的行事曆
const rows = cals.map(cal => ({
name: cal.getName(),
id: cal.getId(),
isPrimary: cal.isMyPrimaryCalendar(),
isOwned: cal.isOwnedByMe(),
isHidden: cal.isHidden()
}));
Logger.log(JSON.stringify(rows, null, 2));
}
說明重點
getAllCalendars() 會回傳整包行事曆,不保證順序。
之後多半會把 id 存起來(例如放到試算表或 Properties),供其他自動化流程使用。
逐步實作流程
步驟 1:建立 Apps Script 專案
開 Google 試算表(可有可無,但方便後續寫入),選單 擴充功能 → Apps Script。
新增檔案 Calendars.gs,貼上上方範例。
首次執行 listAllCalendars(),依提示完成授權(含 Calendar scope)。授權是 Calendar 服務的必備流程。
步驟 2:寫入試算表(便於檢索與 SEO 報表)
function dumpCalendarsToSheet() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sh = ss.getSheetByName('Calendars') || ss.insertSheet('Calendars');
sh.clear();
sh.getRange(1, 1, 1, 5).setValues([['Name', 'ID', 'Primary', 'OwnedByMe', 'Hidden']]);
const cals = CalendarApp.getAllCalendars();
const data = cals.map(cal => [
cal.getName(),
cal.getId(),
cal.isMyPrimaryCalendar(),
cal.isOwnedByMe(),
cal.isHidden()
]);
if (data.length) sh.getRange(2, 1, data.length, 5).setValues(data);
}
步驟 3:過濾條件範例(只取本人擁有的)
function listOwnedCalendarsOnly() {
const owned = CalendarApp.getAllCalendars()
.filter(cal => cal.isOwnedByMe());
owned.forEach(cal => Logger.log(`${cal.getName()} | ${cal.getId()}`));
}
步驟 4:找出主要行事曆並標記
function markPrimaryCalendar() {
const cals = CalendarApp.getAllCalendars();
const primary = cals.find(cal => cal.isMyPrimaryCalendar());
if (primary) {
Logger.log(`Primary: ${primary.getName()} (${primary.getId()})`);
}
}
isMyPrimaryCalendar() 可直接判斷主要行事曆,無須手動比對 ID。
步驟 5:延伸—抓各行事曆「今天」事件數
function countTodayEventsPerCalendar() {
const start = new Date();
start.setHours(0,0,0,0);
const end = new Date();
end.setHours(23,59,59,999);
CalendarApp.getAllCalendars().forEach(cal => {
const events = cal.getEvents(start, end);
Logger.log(`${cal.getName()}: ${events.length} events today`);
});
}
常見使用情境
情境 A:建立「行事曆索引表」供其他腳本引用
把 id、擁有權、是否顯示、顏色(可 getColor())、時區(getTimeZone())等欄位整理到試算表,其他腳本只需查這張表就能快速定位要操作的行事曆。
註:顏色、時區等屬性與設定方法皆可在 Calendar/CalendarApp 類別文件中查到。
情境 B:公司/學校網域的「共用資源日曆」管理
定期掃描所有行事曆,過濾出 isOwnedByMe() === false 但 isHidden() === true 的清單,提醒使用者把重要共用行事曆「顯示」出來,以免錯過。此類治理腳本可排程每日跑一次(時間驅動觸發器)。
情境 C:結合 REST API 做跨系統整合
若你需要把行事曆清單拋到其他系統,或要比對使用者 Calendar List(REST 的 CalendarList.list),可在 Apps Script 端以 UrlFetchApp.fetch() 打 API,用 getAllCalendars() 結果當本地基準做比對。
錯誤與例外處理(訊息、原因、解法)
下面彙整實務最常遇到的狀況,包含授權、ID、權限與資料品質。參考後面的「防呆程式碼」一起使用,能大幅降低部署後的維護成本。
1. 授權不足 / 第一次執行失敗
症狀:執行時跳出授權或直接被拒。
原因:缺乏 Calendar 相關 OAuth scope。
解法:在第一次執行時,依提示完成授權;若是新版專案,確認使用者以具有目標行事曆存取權的帳號授權。
2. 拿到空陣列或「似乎抓不到全部」
症狀:getAllCalendars() 回傳陣列異常少。
原因:帳號本身就只有主要行事曆;或你以錯誤帳號授權;或在前端環境誤用(例如期望在 HTML 端直接拿到完整物件)。
解法:先在單純的伺服端函式中呼叫並 Logger.log 檢查;確認目前授權帳號有訂閱其他行事曆;前端請用 google.script.run 呼叫伺服端函式回傳序列化後的資料(例如轉成物件陣列再回傳),避免直接把 Calendar 物件傳回造成不可序列化的空值。社群也曾討論過「陣列出現 null」的情況,多與傳遞方式或序列化有關。
3. 不是本人擁有的行事曆無法寫入
症狀:對某些行事曆 createEvent() 失敗或被拒。
原因:你只是訂閱者,沒有寫入權限。
解法:使用 isOwnedByMe() 檢查;或請擁有者授予「可變更事件」權限。
4. 主要行事曆判斷錯誤
症狀:用名稱或 ID 比對主要行事曆時失誤。
解法:直接用 isMyPrimaryCalendar() 判斷,不要猜名稱。
5. ID 與 REST API 使用不一致
症狀:拿著 Apps Script 取得的 ID 去打 REST events.get、events.list 失敗。
解法:確認使用的端點正確,清單請看 CalendarList.list,主要行事曆可用 "primary"。
6. 隱藏行事曆導致以為「不見了」
症狀:在 Google Calendar 介面看不到,但 getAllCalendars() 有回傳。
解法:用 isHidden() 檢查;需要顯示可呼叫 setSelected(true) 或在 UI 勾選顯示。
防呆與最佳實務
1. 統一輸出「安全資料結構」
不要直接把 Calendar 物件丟給前端或寫入表格;先轉成純物件(POJO)後再輸出,避免序列化問題。
function getCalendarsAsObjects() {
return CalendarApp.getAllCalendars().map(c => ({
name: c.getName(),
id: c.getId(),
primary: c.isMyPrimaryCalendar(),
owned: c.isOwnedByMe(),
hidden: c.isHidden(),
timeZone: c.getTimeZone()
}));
}
2. 緩存與更新策略
行事曆清單不會天天變,但也不是完全固定。建議:
首次全量抓取 → 寫入試算表
之後以時間驅動觸發器(每日或每週)更新
新增或刪除時以 ID 作為唯一鍵
3. 權限檢查在前
對欲寫入的行事曆,先以 isOwnedByMe() 或嘗試建立最小事件(或用 try/catch)確認權限,再執行批次操作,降低中途失敗。
4. 與 REST API 對齊
如需跨系統整合(例如後端微服務或報表平台):
用 getAllCalendars() 建內部索引
外部系統用 CalendarList.list 做一致性比對
事件層則用 events.list/events.get 查詢與比對。
5. 時區與跨日處理
抓事件時,請用起訖時間界定當日範圍(見上例)。跨時區專案要以行事曆的 getTimeZone() 或專案時區統一處理。
進階範例:建立共用資料表,並標註可操作的目標行事曆
function buildCalendarIndex() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('CalendarIndex') || ss.insertSheet('CalendarIndex');
sh.clear();
sh.appendRow(['Active', 'Name', 'ID', 'Owned', 'Primary', 'Hidden', 'TimeZone']);
CalendarApp.getAllCalendars()
.sort((a, b) => a.getName().localeCompare(b.getName()))
.forEach(c => {
// 預設把「本人擁有」且「非隱藏」的行事曆標記為可操作
const active = c.isOwnedByMe() && !c.isHidden();
sh.appendRow([
active,
c.getName(),
c.getId(),
c.isOwnedByMe(),
c.isMyPrimaryCalendar(),
c.isHidden(),
c.getTimeZone()
]);
});
}
有了這張索引表,其他腳本(例如大量建立事件、月份匯總報表、假期同步)都能快速引用,降低硬編碼的風險。
常見問題與雷點
1. 以名稱當唯一鍵
雷點:同名行事曆在不同人或不同來源很常見。
正解:一律以 getId() 當唯一識別。
2. 把 Calendar 物件直接傳到前端
雷點:HTML 前端常看見 null 或空資料。
正解:轉成 JSON 友善的純物件後再回傳(見上方防呆)。
3. 用猜測方式判斷主要行事曆
雷點:用名稱或排序判斷主要行事曆很容易失準。
正解:呼叫 isMyPrimaryCalendar()。
4. 期望訂閱行事曆可任意寫入
雷點:訂閱不等於擁有寫入權。
正解:isOwnedByMe() 先判斷;必要時調整分享權限。
5. 忽略隱藏狀態
雷點:UI 看不到不代表不存在,腳本仍會抓到。
正解:用 isHidden()/setSelected(true) 管理顯示。
6. REST/Apps Script 混用時搞混端點與 ID
雷點:拿 Apps Script 的 ID 去打錯 REST 端點以致失敗。
正解:清單端點用 CalendarList.list,主要行事曆可用 "primary"。
問題集
Q1:getAllCalendars() 會包含我只讀的行事曆嗎?
會。它回傳「擁有或已訂閱」的所有行事曆;是否能寫入要看權限,可用 isOwnedByMe() 快速區分。
Q2:怎麼可靠地找出主要行事曆?
直接用 isMyPrimaryCalendar(),不要用名稱或排序猜。
Q3:我要打純 REST API,也能拿到清單嗎?
可以,用 CalendarList.list。主要行事曆在事件端點可用 "primary" 當 calendarId。
Q4:第一次跑就報權限錯誤?
Calendar 服務多數方法需要授權,按提示授權正確的帳號即可。
完整示範:整包抓取 → 清洗 → 寫表 → 輸出報表
function fullCalendarInventory() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('CalInventory') || ss.insertSheet('CalInventory');
sh.clear();
const headers = [
'Name','ID','Primary','Owned','Hidden','TimeZone',
'TodayEvents'
];
sh.getRange(1,1,1,headers.length).setValues([headers]);
const start = new Date(); start.setHours(0,0,0,0);
const end = new Date(); end.setHours(23,59,59,999);
const data = CalendarApp.getAllCalendars().map(c => {
const events = c.getEvents(start, end);
return [
c.getName(),
c.getId(),
c.isMyPrimaryCalendar(),
c.isOwnedByMe(),
c.isHidden(),
c.getTimeZone(),
events.length
];
});
if (data.length) sh.getRange(2,1,data.length,headers.length).setValues(data);
Logger.log(`Indexed ${data.length} calendars.`);
}
