function main() { const now = new Date(); const hour = now.getHours(); // 0〜23 const minute = now.getMinutes(); // 0〜59 const isNight = (hour >= 22 || hour < 5); // 22:00〜翌5:00 if (isNight) { // 夜間:5分ごとに1回実行(minute が 0,5,10,15… のときだけ動く) if (minute % 4 !== 0) { return; // 実行せず終了 } } replaceYouTubeURLsInDocument(); } function replaceYouTubeURLsInDocument() { const documentId = '1uOCQbGhxxUw6qkXCLMMARY92Rte4vvt0WniNminzQp8'; const doc = DocumentApp.openById(documentId); const body = doc.getBody(); const youtubeRegex = /(https?:\/\/[^\s]+)/g; const texts = getAllTextElements(body); texts.forEach(text => { let t = text.getText(); const matches = [...t.matchAll(youtubeRegex)]; if (matches.length === 0) return; matches.forEach(match => { const url = match[0]; const videoIdMatch = url.match(/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/); if (!videoIdMatch) return; const videoId = videoIdMatch[1]; // 変更:URL と 表示ラベルを生成する新形式のデータ const result = getVidflyDataFormatted(videoId); if (!result) return; const replacementText = result.text; // Title + formatted list // 元 URL を replacementText に置換して反映 t = t.replace(url, replacementText); text.setText(t); // ハイパーリンクを設定 const insertedPos = t.indexOf(result.title); let cursor = insertedPos + result.title.length + 1; result.links.forEach(item => { const label = item.label; const linkUrl = item.url; const start = t.indexOf(label, cursor); if (start === -1) return; const end = start + label.length - 1; text.setLinkUrl(start, end, linkUrl); cursor = end + 1; }); }); }); console.log("Completed."); } function getAllTextElements(element) { const result = []; const numChild = element.getNumChildren(); for (let i = 0; i < numChild; i++) { const child = element.getChild(i); if (child.getType() === DocumentApp.ElementType.TEXT) { result.push(child.asText()); } else if (child.getNumChildren) { result.push(...getAllTextElements(child)); } } return result; } function getVidflyDataFormatted(videoId) { try { const youtubeUrl = `https://www.youtube.com/watch?v=${videoId}`; const encodedUrl = encodeURIComponent(youtubeUrl); const apiUrl = `https://corsproxy.io/?url=https%3A%2F%2Fapi.vidfly.ai%2Fapi%2Fmedia%2Fyoutube%2Fdownload%3Furl%3D${encodedUrl}`; const response = UrlFetchApp.fetch(apiUrl, { method: 'GET', muteHttpExceptions: true, headers: { 'User-Agent': 'Mozilla/5.0' } }); if (response.getResponseCode() !== 200) return null; const data = JSON.parse(response.getContentText()); if (!data || !data.data || !data.data.items) return null; const title = data.data.title || "タイトルなし"; const links = []; data.data.items.forEach(item => { if (!item.url) return; // 表示名 let label = item.label || "No label"; // ファイルタイプの判定 let fileType = ""; const lowerLabel = label.toLowerCase(); // 動画形式の判定 if (lowerLabel.includes('mp4') || lowerLabel.includes('webm')) { fileType = "(動画)"; } // 音声形式の判定 else if (lowerLabel.includes('mp3') || lowerLabel.includes('m4a') || lowerLabel.includes('opus')) { fileType = "(音声)"; } // type: video_with_audio → 「音声付き」 if (item.type === "video_with_audio") { label = label + "(音声付き)"; } else { label = label + fileType; } links.push({ url: item.url, label: label }); }); // 文書に書き込む text を構築 const lines = []; lines.push(title); // ←最初にタイトルを表示 links.forEach(l => lines.push(l.label)); return { title: title, text: lines.join("\n"), links: links }; } catch (e) { console.error("Vidfly error: " + e); return null; } }