//■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ // // ZOOM APIを使って録画データをダウンロードし、オリジナルデータは削除するスクリプト // 2024-08-25 作成 // // 登録されているユーザーのメールアドレスのフォルダを作り、そのなかにTopic名のフォルダを作成して、 // そこに個別にDLされる。ファイル名は録画開始日時とTOPIC名。 // //■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ //Client ID : 3d605ODWTiKbJRrifQjerA //Client Secret : HjxpKnITWdWQYgAajW8iWgq3YAZ61aLg //Account ID : tpo4aQgXRdedqBGApzPTcA //Verification Toke : CrWw9OrNQYm1QQ8telPNpw const moment = require('moment-timezone'); const axios = require('axios'); const fs = require('fs'); const path = require('path'); const clientId = '3d605ODWTiKbJRrifQjerA' const clientSecret = 'HjxpKnITWdWQYgAajW8iWgq3YAZ61aLg'; const accountId = 'tpo4aQgXRdedqBGApzPTcA'; //■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ //実行する基準の月と月数を指定 //const baseYear = 2024; //const baseMonth = 8 //const baseDate = moment(baseYear + '-' + baseMonth.toString().padStart(2, '0') + '-01T00:00:00Z').utc(); const monthLength = 1; //■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ //Zoom APIのエンドポイント const baseUrl = 'https://api.zoom.us/v2'; const baseDirectory = 'ZoomRecordings'; let accessToken; let saveDirectory; let fromDate; let toDate; //アクセストークンの取得 const getAccessToken = async () => { try { const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); const response = await axios.post('https://zoom.us/oauth/token', null, { headers: { 'Authorization': `Basic ${auth}`, }, params: { grant_type: 'account_credentials', account_id: accountId } }); return response.data.access_token; } catch (error) { console.error('エラー アクセストークンの取得に失敗しました : ', error); } }; //ユーザー一覧取得 async function getUserList() { const response = await axios.get(baseUrl + '/users', { headers: { 'Authorization': `Bearer ${accessToken}` }, params: { //取得したいパラメータを指定 (e.g., type, status) } }); return response.data; } //ミーティングの一覧を取得 //UserIdを指定する async function getMeetingList(userId) { const response = await axios.get(baseUrl + '/users/' + userId + '/meetings', { headers: { 'Authorization': `Bearer ${accessToken}` }, params: { //取得したいパラメータを指定 (e.g., type, status) } }); return response.data; } //ファイルリストの取得 async function getRecordingFileList(uuid) { //ダウンロードリクエストを送信 const response = await axios.get(baseUrl + '/meetings/' + uuid + '/recordings', { headers: { 'Authorization': `Bearer ${accessToken}` }, params: { //取得したいパラメータを指定 (e.g., type, status) } }); return response.data; } //録画リストの一覧を取得 //UserIdを指定する async function getRecordingList(userId) { //ダウンロードリクエストを送信 console.log(`▶▶▶ ${fromDate} ${toDate} の録画リストを取得中...`); const response = await axios.get(baseUrl + '/users/' + userId + '/recordings', { headers: { 'Authorization': `Bearer ${accessToken}` }, params: { //期間を指定 from: fromDate, to: toDate //取得したいパラメータを指定 (e.g., type, status) } }); return response.data; } //録画データをダウンロード async function downloadFile(file, downloadUrl, filePath) { console.log(`▶▶▶ ${downloadUrl} ダウンロード開始`); try { const response = await axios.get(downloadUrl, { responseType: 'stream' }); return new Promise((resolve, reject) => { response.data.pipe(fs.createWriteStream(filePath)) .on('finish', async () => { //ダウンロードできたら、ファイルを削除 await deleteRecording(file.meeting_id, file.id); resolve(); }) .on('error', reject); }); } catch (error) { console.error(`Error downloading ${filePath}:`, error); throw error; } }; //録画データを削除 async function deleteRecording(meetingUuid, recordingId) { //ヘッダーの設定 const headers = { 'Authorization': `Bearer ${accessToken}` }; //二重エンコードする const encodedMeetingUuid = doubleEncodeUUID(meetingUuid); //削除リクエストのURLを作成 const deleteUrl = `https://api.zoom.us/v2/meetings/${encodedMeetingUuid}/recordings/${recordingId}`; try { await axios.delete(deleteUrl, { headers }); console.log(`▶▶▶ 録画データ ${recordingId} を削除しました`); } catch (error) { console.error('Error deleting recording:', error); } } async function recordingFileDownload(recordingList) { for (meeting of recordingList.meetings) { let startTime = meeting.start_time; startTime = startTime.replaceAll(':', '.'); startTime = startTime.replaceAll('Z', ''); startTime = startTime.replaceAll('T', ' '); let topicName = sanitizeFolderName(meeting.topic); let saveFolder = startTime + ' ' + topicName + ' ' + meeting.id; //フォルダが存在しない場合に作成 fs.mkdir(path.join(saveDirectory, saveFolder), { recursive: true }, (err) => { if (err) { console.error('Error creating directory:', err); } else { //console.log('Directory created successfully!'); } }); console.log('▶▶▶ ' + saveFolder + ' 録画データのダウンロード開始'); for (file of meeting.recording_files) { let startTime = file.recording_start; startTime = startTime.replaceAll(':', '.'); startTime = startTime.replaceAll('Z', ''); startTime = startTime.replaceAll('T', ' '); let downloadUrl = file.download_url + '?access_token=' + accessToken; let fileName = startTime + ' ' + topicName + ' ' + meeting.id + '.' + file.file_extension; let filePath = path.join(saveDirectory, saveFolder + '/' + fileName); //console.log(file); console.log("▶▶▶ " + fileName + ' ダウンロード開始'); await downloadFile(file, downloadUrl, filePath); } console.log('▷▷▷ ' + saveFolder + ' 録画データのダウンロード完了'); } } //メイン処理 async function main() { let baseYear; let baseMonth; if (process.argv.length == 4) { baseYear = process.argv[2]; baseMonth = process.argv[3]; if (isNaN(baseYear) || isNaN(baseMonth)) { console.log("■■■■■■■■■■■■■■■■■■■■■■■■■■"); console.log("■ 引数が数字ではありません 実行例↓"); console.log("■ node .\\実行ファイル名 年 月"); console.log("■ 年と月は数字のみ(前0不要)"); console.log("■■■■■■■■■■■■■■■■■■■■■■■■■■"); return false; } else { baseYear = parseFloat(baseYear); baseMonth = parseFloat(baseMonth); if (baseYear <= 2000 || baseYear > 3000) { console.log("■■■■■■■■■■■■■■■■■■■■■■■■■■"); console.log("■ 引数が数字ではありません 実行例↓"); console.log("■ node .\\実行ファイル名 年 月"); console.log("■ 年は西暦4桁で指定してください"); console.log("■■■■■■■■■■■■■■■■■■■■■■■■■■"); return false; } if (baseMonth < 1 || baseMonth > 12) { console.log("■■■■■■■■■■■■■■■■■■■■■■■■■■"); console.log("■ 引数が数字ではありません 実行例↓"); console.log("■ node .\\実行ファイル名 年 月"); console.log("■ 月は1~12で指定してください"); console.log("■■■■■■■■■■■■■■■■■■■■■■■■■■"); return false; } } } else { console.log("■■■■■■■■■■■■■■■■■■■■■■■■■■"); console.log("■ 引数を正確に指定してください 実行例↓"); console.log("■ node .\\実行ファイル名 年 月"); console.log("■ 年と月は数字のみ(前0不要)"); console.log("■■■■■■■■■■■■■■■■■■■■■■■■■■"); return false; } let baseDate = moment(baseYear + '-' + baseMonth.toString().padStart(2, '0') + '-01T00:00:00Z').utc(); let fromDate0 = baseDate.clone(); let toDate0 = baseDate.clone().add(1, 'months').subtract(1, 'days'); //let toDate0 = baseDate.clone().add(1, 'months'); console.log("★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★"); console.log("★ ZOOM録画データのダウンロード開始"); console.log("■baseDate : " + baseDate); //console.log("■ 検証のため停止"); //return false; for (i = 0; i < monthLength; i++) { fromDate = fromDate0.format('YYYY-MM-DD');; toDate = toDate0.format('YYYY-MM-DD');; console.log(fromDate + ' ' + toDate); try { //アクセストークンの取得 accessToken = await getAccessToken(); //console.log(accessToken); //ユーザー情報一覧取得 const userList = await getUserList(); //console.log(userList); //ユーザーIDを指定してミーティングリストの取得 //const meetingList = await getMeetingList('qQ0Tr6EHTBiYxOkSSd_CUw'); //console.log(meetingList); //ミーティングIDを指定してファイルリストの取得 //const fileList = await getRecordingFileList('tmqmRfofTYSjU2AvjR7j/A=='); //console.log(fileList); for (user of userList.users) { saveDirectory = baseDirectory; saveDirectory = path.join(saveDirectory, user.email); let recordingList; //録画データがなくなるまで繰り返す do { recordingList = await getRecordingList(user.id); if (recordingList.meetings.length > 0) { console.log("■■■ " + user.id + ' ' + user.display_name + ' ' + user.email); console.log("▶▶▶ 録画データ数 :" + recordingList.meetings.length); console.log(recordingList.meetings); await recordingFileDownload(recordingList); } } while (recordingList.meetings.length > 0); } //録画リストの取得 //UserIDを指定する //const recordingList = await getRecordingList('E4UdyiFuRvuvm_I4K-WqLQ'); //console.log(recordingList); //取得した録画リストをダウンロードして削除する //await recordingFileDownload(recordingList); } catch (error) { console.error(error); } //月を1つ進める fromDate0 = fromDate0.clone().add(1, 'months'); toDate0 = toDate0.clone().add(1, 'months'); } console.log("★ ZOOM録画データのダウンロード終了"); console.log("★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★"); } //URLエンコードを2回行う function doubleEncodeUUID(uuid) { return encodeURIComponent(encodeURIComponent(uuid)); } //フォルダ名に使えない文字列の排除 function sanitizeFolderName(str) { // フォルダ名に使用できない一般的な文字の正規表現 const invalidCharsRegex = /[/\\:*?"<>|]/g; // 空白をハイフンに置換 const sanitizedStr = str.replace(/\s+/g, '-'); // 不適切な文字を削除 return sanitizedStr.replace(invalidCharsRegex, ''); } //処理開始 main();