# [メタ情報] # 識別子: マイライブラリ_生成更新処理_exe # システム名: マイライブラリ_生成更新処理 # 技術種別: Misc # 機能名: Misc # 使用言語: GAS # 状態: 実行用 # [/メタ情報] M1 Mac M2 Mac共通 バンドル:F1_メディアライブラリ GAS mediaLibary.gs トリガー: processWorkingRecord 5分毎 /** * ============================== * 📌 mediaLibrary.gs(安定版 / 20:50) * ============================== */ /** * ============================== * 📌 ユーティリティ関数 (共通処理) * ============================== */ function safeTrim(v){ return (v == null ? "" : String(v)).trim(); } function generateUniqueWpidex(extn, existingWpidexList) { Logger.log(`🔍 generateUniqueWpidex() 実行 - extn: ${extn}`); if (!existingWpidexList || !Array.isArray(existingWpidexList)) existingWpidexList = []; let existingSet = new Set(existingWpidexList.flat().filter(x => typeof x === "string" && x.trim() !== "")); if (!extn || extn.trim() === "") extn = "tmp"; const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let wpidex, isUnique = false, attempts = 0; while (!isUnique) { attempts++; if (attempts > 10) return "DEFAULT_WPIDEX." + extn; wpidex = Array.from({ length: 8 }, () => chars.charAt(Math.floor(Math.random() * chars.length))).join("") + "." + extn; isUnique = !existingSet.has(wpidex); } return wpidex; } function generateRandomWpid() { return Array.from({ length: 8 }, () => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".charAt(Math.floor(Math.random() * 62))).join(""); } function getCurrentTimestamp() { return Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss"); } /** (D) ファイルパスから `pmedia` または `mmedia` を抽出 */ function extractMediaCategory(filePath) { let match = String(filePath).match(/\/(mmedia|pmedia)\//); return match ? match[1] : "unknown"; } /** (E) URL エンコード(NFC 正規化) */ function encodeUrl(url) { return encodeURI(String(url).normalize("NFC")); } /** (I) `sheet2` から拡張子の `select_no` を取得 */ function getSelectNo(extn, sheet2) { if (!sheet2) { Logger.log("❌ 拡張子リスト (sheet2) が取得できません。"); return "1"; } let extList = sheet2.getDataRange().getValues(); let selectNo = "1"; for (let i = 1; i < extList.length; i++) { if (extList[i][0] === extn) return extList[i][1]; if (extList[i][0] === "others") selectNo = extList[i][1]; } return selectNo; } /** (D) `filePath` から `mmedia` または `pmedia` を抽出し、それに続くパスを取得 */ function extractMediaPath(filePath) { let match = String(filePath).match(/\/(mmedia|pmedia)\/(.+)/); return match ? match[1] + "/" + match[2] : ""; } /** (D) `wp_pathlink_url` を生成 */ function generateWpPathlinkUrl(filePath) { let mediaPath = extractMediaPath(filePath); return "https://xxxxxxxx.com/wp-content/" + mediaPath; } /** 1列検索(等価比較) */ function findRowInColumn(data, columnIndex, searchValue, mode = "DEFAULT") { for (let i = 1; i < data.length; i++) { if (mode === "EXCLUDE_ADD" && data[i][10] === "ADD") continue; if (safeTrim(data[i][columnIndex - 1]) === safeTrim(searchValue)) return i + 1; } return 0; } /** 値の存在判定 */ function findInColumn(data, columnIndex, valueToFind) { for (let i = 1; i < data.length; i++) { if (safeTrim(data[i][columnIndex - 1]) === safeTrim(valueToFind)) return true; } return false; } /** 異なる列組み合わせ検索 */ function findRowInColumnDifferent(data, columnIndex, value, matchValue) { for (let i = 1; i < data.length; i++) { if (safeTrim(data[i][columnIndex - 1]) !== safeTrim(value) && safeTrim(data[i][0]) === safeTrim(matchValue)) { return i + 1; } } return 0; } /** 除外行を指定して検索(そのまま比較版) */ function findRowInColumnExcluding(data, columnIndex, searchValue, excludeRows) { for (let i = 1; i < data.length; i++) { if (excludeRows.includes(i + 1)) continue; if (safeTrim(data[i][columnIndex - 1]) === safeTrim(searchValue)) return i + 1; } return 0; } /** * ============================== * 🔧 追加ユーティリティ(表記ゆれ吸収) * ============================== */ /** パス正規化(%20/空白、連続スラ、NFC差、末尾スラ、?v= などを吸収) */ function normalizePath(p) { if (!p) return ""; try { p = decodeURIComponent(p); } catch (e) {} p = String(p).normalize("NFC").trim(); p = p.replace(/\\/g, "/").replace(/\/{2,}/g, "/"); p = p.replace(/\?v=\d+$/i, ""); if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1); return p; } /** 正規化後の basename 取得 */ function baseNameFromPath(p) { const n = normalizePath(p); const idx = n.lastIndexOf("/"); return idx >= 0 ? n.slice(idx + 1) : n; } /** 正規化検索(1列、1始まり列番号を指定) */ function findRowByNormalizedPath(data, colIndex1Based, searchValue) { const want = normalizePath(searchValue); for (let r = 1; r < data.length; r++) { const cur = normalizePath(data[r][colIndex1Based - 1]); if (cur && cur === want) return r + 1; // 1始まりで返す } return 0; } /** 正規化 + 除外行 指定版 */ function findRowInColumnExcludingNormalized(data, colIndex1Based, searchValue, excludeRows) { const want = normalizePath(searchValue); for (let r = 1; r < data.length; r++) { if (excludeRows && excludeRows.includes(r + 1)) continue; const cur = normalizePath(data[r][colIndex1Based - 1]); if (cur && cur === want) return r + 1; } return 0; } /** * ============================== * 📌 メイン処理 (doPost → processWorkingRecord) * ============================== */ /** * videoembed.json の再生成(+送信)を安全に呼び出すラッパ * - 今後、rebuildVideoembedJson() を実装/追記すれば自動でそちらを使います * - 互換として exportVideoembedJson() / generateEmbedJSON() も探して呼びます * - postVideoembedJsonToXserver_() があれば送信まで実施 */ function triggerVideoembedRebuild_() { try { if (typeof rebuildVideoembedJson === 'function') { Logger.log('🧩 trigger: rebuildVideoembedJson()'); return !!rebuildVideoembedJson(); // ← 推奨:生成+送信まで内部で完了させる実装 } if (typeof exportVideoembedJson === 'function') { Logger.log('🧩 trigger: exportVideoembedJson()'); exportVideoembedJson(); // 生成(Drive保存など) if (typeof postVideoembedJsonToXserver_ === 'function') { Logger.log('🧩 trigger: postVideoembedJsonToXserver_()'); postVideoembedJsonToXserver_(); // 送信 } return true; } if (typeof generateEmbedJSON === 'function') { Logger.log('🧩 trigger: generateEmbedJSON()'); generateEmbedJSON(); // 生成(Drive保存など) if (typeof postVideoembedJsonToXserver_ === 'function') { Logger.log('🧩 trigger: postVideoembedJsonToXserver_()'); postVideoembedJsonToXserver_(); // 送信 } return true; } Logger.log('ℹ️ videoembed 再生成関数が見つかりません(rebuildVideoembedJson/exportVideoembedJson/generateEmbedJSON 未定義)'); return false; } catch (err) { Logger.log('❌ triggerVideoembedRebuild_ エラー: ' + err); return false; } } /** * Webhook 受け口 * - 既存フロー(F1: 作業行追加 → processWorkingRecord)を維持 * - 加えて、モードに応じて videoembed.json の再生成&送信を実行 * - mode: "EMBED_ONLY" → 動画埋め込みJSONだけ更新 * - mode: "FULL_REBUILD" → ライブラリ再生成+整頓の後、動画埋め込みJSON更新 * - それ以外(通常) → 作業行追加&処理の後、動画埋め込みJSON更新 */ function doPost(e) { Logger.log('🚀 doPost() 実行開始'); // ===== バリデーション ===== if (!e || !e.postData || !e.postData.contents) { Logger.log('⚠️ postData missing'); return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'postData missing' })) .setMimeType(ContentService.MimeType.JSON); } // ===== 受信データをJSON化 ===== let data; try { data = JSON.parse(e.postData.contents); } catch (parseErr) { Logger.log('❌ JSON parse error: ' + parseErr); return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'invalid json' })) .setMimeType(ContentService.MimeType.JSON); } Logger.log('📥 受信データ: ' + JSON.stringify(data, null, 2)); try { // ===== モード分岐 ===== const mode = String(data.mode || '').toUpperCase(); // ---- (A) 動画埋め込みJSONだけ更新(テスト/本番の軽量更新)---- if (mode === 'EMBED_ONLY') { Logger.log('🧩 EMBED_ONLY → videoembed.json を更新(ライブラリは触らない)'); const ok = triggerVideoembedRebuild_(); return ContentService.createTextOutput(JSON.stringify({ status: ok ? 'success' : 'noop', mode })) .setMimeType(ContentService.MimeType.JSON); } // ---- (B) フル再構築(ライブラリ更新→整頓→videoembed更新)---- if (mode === 'FULL_REBUILD') { Logger.log('🔧 FULL_REBUILD → ライブラリ JSON 再生成 → 整頓 → videoembed 更新'); // 既存:ライブラリ(F1)側のフル更新 if (typeof updateJsonFile === 'function') updateJsonFile(); if (typeof cleanMediaLibrary === 'function') cleanMediaLibrary(); // 追加:動画埋め込みJSONの再生成&送信 const ok = triggerVideoembedRebuild_(); return ContentService.createTextOutput(JSON.stringify({ status: ok ? 'success' : 'partial', mode })) .setMimeType(ContentService.MimeType.JSON); } // ===== 通常フロー:作業用行を追加 → processWorkingRecord() ===== const ss = SpreadsheetApp.openById('<あなたのspreadsheetID>'); // F1ブック const sheet1 = ss.getSheetByName('sheet1'); let lastRow = sheet1.getLastRow(); // 入力の正規化 let filePath = (data.filePath || '').trim(); let dropboxLink = (data.dropboxLink || '').trim(); let fileExtn = (data.extn || '').trim(); // fileName は filePath から自動抽出(フォールバック: "unknown") let fileName = (filePath.split('/').pop() || '').trim() || 'unknown'; // ext が無ければ fileName から補完(ドット無しなら "unknown") if (!fileExtn) fileExtn = fileName.includes('.') ? fileName.split('.').pop() : 'unknown'; // 最低限の入力チェック(通常フローではどちらかは欲しい) if (!filePath && !dropboxLink) { return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'filePath or dropboxLink is required' })).setMimeType(ContentService.MimeType.JSON); } // (B) 既存 wpidex リスト let valuesB = (lastRow > 0) ? sheet1.getRange(1, 2, lastRow, 1).getValues() : []; // 一意な wpidex を生成(既存ユーティリティを使用) let newWpidex = generateUniqueWpidex(fileExtn, valuesB); // (D) wp_pathlink_url(/wp-content/ 以降の相対パスに組み立て) let mediaPath = extractMediaPath(filePath); let wpPathlinkUrl = 'https://xxxxxxxx.com/wp-content/' + mediaPath; // (E) URLエンコード版 let encodedUrl = encodeUrl(wpPathlinkUrl); // (I) selectNo(拡張子ごとの既定振り分け) const sheet2 = ss.getSheetByName('sheet2'); let selectNo = getSelectNo(fileExtn, sheet2); let currentTime = getCurrentTimestamp(); let newRow = lastRow + 1; // 作業用レコードを追加(K=ADD、N=初期時刻) sheet1.getRange(newRow, 1, 1, 14).setValues([[ dropboxLink, // A newWpidex, // B 'https://xxxxxxxx.com/rd.php?id=' + newWpidex, // C wpPathlinkUrl, // D encodedUrl, // E filePath, // F fileName, // G fileExtn, // H selectNo, // I currentTime, // J 'ADD', // K 'https://xxxxxxxx.com/rd.php?id=' + newWpidex, // L '', // M currentTime // N ]]); SpreadsheetApp.flush(); Logger.log('✅ 作業用レコード追加 - 行 ' + newRow); // 直ちに処理(F3消化→K=ADD分岐→JSON更新→整頓) processWorkingRecord(); Logger.log('✅ processWorkingRecord() 呼び出し完了'); // 追加:通常フローの最後にも動画埋め込みJSON更新を実施 const ok = triggerVideoembedRebuild_(); return ContentService.createTextOutput(JSON.stringify({ status: 'success', videoembedUpdated: !!ok })) .setMimeType(ContentService.MimeType.JSON); } catch (error) { Logger.log('❌ doPost() でエラー発生: ' + error); return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: String(error) })) .setMimeType(ContentService.MimeType.JSON); } } /** * ============================== * 📌 作業用レコードの処理 (processWorkingRecord) — 安定版 * - F3(D=TRUE) は「旧Fパスに完全一致した1行のみ」更新 * - 既存の (1)〜(5) の ADD 分岐を実行 * - 変更があれば JSON 更新 → ライブラリ整理 * ============================== */ function processWorkingRecord() { Logger.log("🚀 processWorkingRecord() 実行開始"); const ss = SpreadsheetApp.openById("<あなたのspreadsheetID>"); const sheet1 = ss.getSheetByName("sheet1"); const ssF3 = SpreadsheetApp.openById("<あなたのID>"); const sheetF3 = ssF3.getSheetByName("sheet1"); let f1Data = sheet1.getDataRange().getValues(); let f3Data = sheetF3.getDataRange().getValues(); let lastRow = f1Data.length; let updateRequired = false; // ========================= A) F3(ファイルパス変更履歴)の消化(単体一致のみ) if (f3Data && f3Data.length > 0) { for (let i = 0; i < f3Data.length; i++) { // ヘッダ無し運用想定のため i=0 から const beforePath = safeTrim(f3Data[i][0] || ""); const afterPath = safeTrim(f3Data[i][1] || ""); const flagRaw = f3Data[i][3]; const flag = String(flagRaw).toUpperCase(); if (flag !== "TRUE") continue; if (!beforePath || !afterPath) continue; // 旧パスに一致する F1 行(F列=6)を正規化比較で検索(完全一致) const f1Row = findRowByNormalizedPath(f1Data, 6, beforePath); if (f1Row > 0) { const normalizedAfter = normalizePath(afterPath); const newFileName = baseNameFromPath(normalizedAfter); const extn = newFileName.includes(".") ? newFileName.split(".").pop() : ""; const wpPathlinkUrl = generateWpPathlinkUrl(normalizedAfter); const encodedUrl = encodeUrl(wpPathlinkUrl); Logger.log(`🟢 F3適用: F1行 ${f1Row} を更新 ${beforePath} → ${afterPath}(wpidexは不変)`); // B(wpidex) は触らない。E/F/G/H/J/K のみ更新 sheet1.getRange(f1Row, 5).setValue(encodedUrl); // E sheet1.getRange(f1Row, 6).setValue(normalizedAfter); // F sheet1.getRange(f1Row, 7).setValue(newFileName); // G sheet1.getRange(f1Row, 8).setValue(extn); // H sheet1.getRange(f1Row,10).setValue(getCurrentTimestamp()); // J sheet1.getRange(f1Row,11).setValue("CHG"); // K // F3 側は消化済みへ sheetF3.getRange(i + 1, 4).setValue("FALSE"); updateRequired = true; } else { Logger.log(`⚠️ F3適用スキップ: 旧パスが F1 に見当たりません → ${beforePath}`); } } } if (updateRequired) { SpreadsheetApp.flush(); Utilities.sleep(300); f1Data = sheet1.getDataRange().getValues(); lastRow = f1Data.length; } // ========================= B) 既存の K=ADD 分岐 (1)〜(5) let workRecordRow = -1; for (let i = 1; i < lastRow; i++) { if (f1Data[i][10] === "ADD") { workRecordRow = i + 1; break; } } if (workRecordRow !== -1) { Logger.log(`🟢 作業用レコードの行番号: ${workRecordRow}`); for (let i = 1; i < lastRow; i++) { if (f1Data[i][10] !== "ADD") continue; let filePath = safeTrim(f1Data[i][5]); // F let dropboxLink = safeTrim(f1Data[i][0]); // A let rowNum = i + 1; Logger.log(`🟢 (1) 処理開始 - 行: ${rowNum}, ファイルパス: ${filePath}`); // (1)F3(B) に filePath が存在するか(正規化版) let f3Row = findRowInColumnExcludingNormalized(f3Data, 2, filePath, [1]); if (f3Row > 0) { let f3AValue = safeTrim(f3Data[f3Row - 1][0]); // 旧パス Logger.log(`✅ (1) F3(B) に一致 - F3(A): ${f3AValue}`); let f1Row = findRowInColumnExcludingNormalized(f1Data, 6, f3AValue, [1, workRecordRow]); if (f1Row > 0) { Logger.log(`✅ (1) F1(F) に一致 - 行 ${f1Row}`); let updatedValues = [ dropboxLink, // A f1Data[rowNum - 1][3], // D f1Data[rowNum - 1][4], // E f1Data[rowNum - 1][5], // F f1Data[rowNum - 1][6], // G f1Data[rowNum - 1][8], // I f1Data[rowNum - 1][9], // J "CHG" // K ]; sheet1.getRange(f1Row, 1).setValue(updatedValues[0]); // A sheet1.getRange(f1Row, 4).setValue(updatedValues[1]); // D sheet1.getRange(f1Row, 5).setValue(updatedValues[2]); // E sheet1.getRange(f1Row, 6).setValue(updatedValues[3]); // F sheet1.getRange(f1Row, 7).setValue(updatedValues[4]); // G sheet1.getRange(f1Row, 9).setValue(updatedValues[5]); // I sheet1.getRange(f1Row,10).setValue(updatedValues[6]); // J sheet1.getRange(f1Row,11).setValue(updatedValues[7]); // K // 作業用は DEL sheet1.getRange(rowNum, 11).setValue("DEL"); updateRequired = true; Logger.log(`✅ (1) F1 レコード更新完了 - 行 ${f1Row}`); } } // (2)F1(F) と F1(A) が同じ行に存在 let existingRowF = findRowInColumnExcludingNormalized(f1Data, 6, filePath, [1, workRecordRow]); let existingRowA = findRowInColumnExcluding(f1Data, 1, dropboxLink, [1, workRecordRow]); Logger.log(`🔍 (2) チェック - F1(F): ${existingRowF}, F1(A): ${existingRowA}`); if (existingRowF > 0 && existingRowA > 0 && existingRowF === existingRowA) { Logger.log(`✅ (2) 完全一致 → 作業用レコードを DEL`); sheet1.getRange(rowNum, 11).setValue("DEL"); updateRequired = true; continue; } // (3)F1(F) あり & F1(A) に dropboxLink なし let filePathExists = existingRowF > 0; let dropboxNotExists = (existingRowA === 0) || (existingRowA > 0 && safeTrim(f1Data[existingRowA - 1][0]) !== dropboxLink); Logger.log(`🔍 (3) チェック - filePathExists: ${filePathExists}, dropboxNotExists: ${dropboxNotExists}`); if (filePathExists && dropboxNotExists) { Logger.log(`✅ (3) 既存行を更新 - 行 ${existingRowF}`); sheet1.getRange(existingRowF, 1).setValue(dropboxLink); // A sheet1.getRange(existingRowF, 9).setValue(f1Data[rowNum - 1][8]); // I sheet1.getRange(existingRowF,10).setValue(f1Data[rowNum - 1][9]); // J sheet1.getRange(existingRowF,11).setValue("CHG"); // K sheet1.getRange(rowNum, 11).setValue("DEL"); // 作業用は DEL updateRequired = true; continue; } // (4)F1(F) なし & F1(A) に dropboxLink 一致 → D〜K を一括更新 let filePathNotExists = existingRowF === 0; let dropboxExists = existingRowA > 0; Logger.log(`🔍 (4) チェック - filePath 不一致: ${filePathNotExists}, dropboxLink 一致: ${dropboxExists}`); if (filePathNotExists && dropboxExists) { Logger.log(`✅ (4) 既存レコード更新 - 行 ${existingRowA}`); let updateData = [ f1Data[rowNum - 1][3], // D f1Data[rowNum - 1][4], // E f1Data[rowNum - 1][5], // F f1Data[rowNum - 1][6], // G f1Data[rowNum - 1][7], // H f1Data[rowNum - 1][8], // I f1Data[rowNum - 1][9], // J "CHG" // K ]; sheet1.getRange(existingRowA, 4, 1, updateData.length).setValues([updateData]); sheet1.getRange(rowNum, 11).setValue("DEL"); updateRequired = true; continue; } // (5)どちらにも存在しない → 作業用行は ADD2 に Logger.log(`✅ (5) 新規候補 → 作業用行を ADD2 に変更`); sheet1.getRange(rowNum, 11).setValue("ADD2"); updateRequired = true; } } else { Logger.log("ℹ️ K=ADD なし(今回のサイクルは F3 のみを処理)"); } // ========================= C) 変更があれば JSON/整理 if (updateRequired) { Logger.log("🔄 シートの変更を適用します。"); SpreadsheetApp.flush(); Utilities.sleep(2000); Logger.log("🔄 JSON データを更新します。"); try { updateJsonFile(); // JSON の更新 Logger.log("✅ JSON 更新完了"); Utilities.sleep(1000); Logger.log("🔄 メディアライブラリの整理を開始します。"); cleanMediaLibrary(); // ライブラリの整理 Logger.log("✅ メディアライブラリの整理完了"); } catch (error) { Logger.log(`❌ JSON/整理 更新エラー: ${error.message}`); } } else { Logger.log("ℹ️ 更新対象なし → 終了"); } } /** * ============================== * 📌 テスト関数 (testDoPost) * ============================== */ function testDoPost() { Logger.log("🚀 testDoPost() 実行開始"); let testData = { dropboxLink: "https://www.dropbox.com/scl/fi/b7vq2icii9n9kz92tdzaz/250307_.png?rlkey=67m44ig1dn6ql59od7zysvkjl&raw=1", filePath: "/Volumes/NO3_SSD/Dropbox/dropbox_1/pmedia/250307_セキレイ.png", fileName: "250307_セキレイ.png", extn: "png", updateFlag: "ADD" }; let mockEvent = { postData: { contents: JSON.stringify(testData) } }; let response = doPost(mockEvent); Logger.log("📩 doPost() のレスポンス: " + response.getContent()); processWorkingRecord(); Logger.log("✅ processWorkingRecord() 実行完了"); SpreadsheetApp.flush(); Utilities.sleep(2000); Logger.log("🔄 JSON ファイルの更新を開始します"); try { updateJsonFile(); Logger.log("✅ JSON ファイルの更新が完了しました"); } catch (error) { Logger.log(`❌ JSON 更新エラー: ${error.message}`); } return response; } /** * ============================== * 📌 JSON生成処理(強制上書き + ログ付き) * ============================== */ function updateJsonFile() { Logger.log("🚀 updateJsonFile() 開始"); try { const ss = SpreadsheetApp.openById("<あなたのspreadsheetID>"); const sheet1 = ss.getSheetByName("sheet1"); let f1Data = sheet1.getDataRange().getValues(); Logger.log(`📜 データ取得 - 行数: ${f1Data.length}`); if (f1Data.length === 0) { Logger.log("⚠️ シート空 → 中止"); return; } let jsonData = []; let hasRow = false; for (let i = 1; i < f1Data.length; i++) { if (f1Data[i][10] === "DEL") continue; // DEL は除外 hasRow = true; jsonData.push({ dropboxlink_url: f1Data[i][0], wpidex: f1Data[i][1], wprun_url: f1Data[i][2], wp_pathlink_url: f1Data[i][3], wp_encoded_url: f1Data[i][4], file_path: f1Data[i][5], file_name: f1Data[i][6], extn: f1Data[i][7], select_no: f1Data[i][8], date_time: f1Data[i][9], column_M: f1Data[i][12], // M column_N: f1Data[i][13] // N }); } if (!hasRow) { Logger.log("⚠️ JSON対象行なし(DEL以外が0): 書き込み中止"); return; } const jsonString = JSON.stringify(jsonData, null, 2); // Googleドライブへの書き込み(File ID は固定) const FILE_ID = "<あなたのID>"; // dropbox_wp_library.json const file = DriveApp.getFileById(FILE_ID); if (!file) { Logger.log("❌ JSON ファイルIDが不正"); return; } Logger.log("[JSON:BEFORE] name=%s id=%s mtime=%s size=%s", file.getName(), file.getId(), file.getLastUpdated(), file.getSize()); // ★強制上書き(内容同一でも mtime を動かす) file.setContent(jsonString); SpreadsheetApp.flush(); Utilities.sleep(1000); const fileAfter = DriveApp.getFileById(FILE_ID); Logger.log("[JSON:AFTER ] name=%s id=%s mtime=%s size=%s url=%s", fileAfter.getName(), fileAfter.getId(), fileAfter.getLastUpdated(), fileAfter.getSize(), fileAfter.getUrl()); Logger.log("✅ JSON 更新完了"); } catch (error) { Logger.log(`❌ updateJsonFile() エラー: ${error.message}`); } } /** * ============================== * 📌 ライブラリの整理(安全版) * ============================== */ function cleanMediaLibrary() { Logger.log("🚀 メディアライブラリの整理を開始"); const ss = SpreadsheetApp.openById("<あなたのspreadsheetID>"); const sheet1 = ss.getSheetByName("sheet1"); let data = sheet1.getDataRange().getValues(); if (!data || data.length === 0) { Logger.log("⚠️ 空データ"); return; } let headers = data[0]; let newData = []; // 1) DEL を削除 data.slice(1).forEach(row => { if (row[10] !== "DEL") newData.push(row); }); // 2) 空白行を除外 newData = newData.filter(row => row.some(cell => cell !== "")); // 3) K列を FALSE へ newData.forEach(row => { if (row[10] !== "FALSE") row[10] = "FALSE"; }); // 4) 重複除去(A+F 同じで N 新しい方優先) let uniqueMap = new Map(); newData.forEach(row => { let key = String(row[0]) + "___" + String(row[5]); let currentTime = new Date(row[13]); if (!uniqueMap.has(key)) { uniqueMap.set(key, row); } else { let existing = uniqueMap.get(key); let existingTime = new Date(existing[13]); if (currentTime > existingTime) uniqueMap.set(key, row); } }); newData = Array.from(uniqueMap.values()); // 5) J列(更新日時)降順 newData.sort((a, b) => new Date(b[9]) - new Date(a[9])); // 6) 書き戻し sheet1.clearContents(); sheet1.getRange(1, 1, 1, headers.length).setValues([headers]); if (newData.length > 0) sheet1.getRange(2, 1, newData.length, newData[0].length).setValues(newData); Logger.log("✅ メディアライブラリの整理が完了"); } /***** videoembed.json 送信用の設定(必要に応じて直してください) *****/ const VIDEOEMBED_SS_ID = '<あなたのspreadsheetID>'; // 同じブックならこのままでOK const VIDEOEMBED_SHEET = '動画パッケージ'; // 埋め込みの元データがあるシート名 // A列=videoid, B列=埋め込みHTML の想定 const VIDEOEMBED_COLS = { videoid: 0, embed: 1 }; // Xserver 側の受け口 const XSERVER_EMBED_ENDPOINT = 'https://xxxxxxxx.com/update_videoembed.php'; const XSERVER_EMBED_TOKEN = ""; /** videoembed.json 用の配列をシートから組み立て */ function buildVideoembedPayload_() { const ss = SpreadsheetApp.openById(VIDEOEMBED_SS_ID); const sh = ss.getSheetByName(VIDEOEMBED_SHEET); if (!sh) throw new Error('動画パッケージ シートが見つかりません: ' + VIDEOEMBED_SHEET); const values = sh.getDataRange().getValues(); const out = []; for (let r = 1; r < values.length; r++) { const row = values[r]; const videoid = String(row[VIDEOEMBED_COLS.videoid] || '').trim(); const embed = String(row[VIDEOEMBED_COLS.embed] || '').trim(); if (!videoid || !embed) continue; if (embed.includes('@@')) continue; // プレースホルダが残っているものは除外 out.push({ videoid, embed }); } Logger.log(`🧰 videoembed payload rows=${out.length}`); return out; } /** Xserver の update_videoembed.php に POST(0件なら送らない) */ function postVideoembedJsonToXserver_() { const payload = buildVideoembedPayload_(); if (payload.length === 0) { Logger.log('⚠️ videoembed: 送信0件のため中断'); return { sent: false, count: 0 }; } const url = XSERVER_EMBED_ENDPOINT + '?token=' + encodeURIComponent(XSERVER_EMBED_TOKEN); const res = UrlFetchApp.fetch(url, { method: 'post', contentType: 'application/json; charset=utf-8', payload: JSON.stringify(payload), muteHttpExceptions: true, }); Logger.log(`📡 videoembed POST → code=${res.getResponseCode()}`); Logger.log(res.getContentText()); if (res.getResponseCode() >= 300) throw new Error('videoembed POST 失敗: ' + res.getResponseCode()); return { sent: true, count: payload.length }; } /** doPost() から呼べる“統一口” — 再生成&送信 */ function rebuildVideoembedJson() { return postVideoembedJsonToXserver_(); }