# [メタ情報] # 識別子: マイライブラリ_生成更新処理_exe # システム名: マイライブラリ_生成更新処理 env終了 # 技術種別: Misc # 機能名: Misc # 使用言語: GAS # 状態: 公開用 # [/メタ情報] 要約: このGoogle Apps Scriptファイル「mediaLibary.gs」は、主にメディアライブラリのデータ管理と外部サービス連携を自動化します。`processWorkingRecord`関数は1分毎のトリガーで実行され、AppSheetとの連携も考慮されているため、デプロイ(新バージョンの選択)が必須です。 主な機能は以下の通りです。 1. **doPost()**: Webhookの受け口となり、受信したデータに基づき処理を分岐します。「EMBED_ONLY」モードでは動画埋め込みJSONのみを更新、「FULL_REBUILD」モードではライブラリの再構築と動画埋め込みJSONの更新を行います。通常フローでは、F1シートに新規作業レコード(K列が"ADD")を追加し、`processWorkingRecord()`を呼び出します。 2. **processWorkingRecord()**: F1シート上の作業レコードを処理するメイン関数です。F3シートのファイルパス変更履歴を消化してF1シートを更新したり、K列が"ADD"のレコードについて既存データとの照合を行い、F1シートの更新("CHG")または新規追加("ADD2")を行います。処理の最後には、`dropbox_wp_library.json`を生成してGoogleドライブに保存、メディアライブラリデータの整理、およびF1シートと`pcloudid`シートの同期を実行します。 3. **動画埋め込みJSONの管理**: `rebuildVideoembedJson`関数を通じて、動画埋め込み用のJSONデータを生成し、指定されたXserverエンドポイントへPOST送信します。 4. **ユーティリティ関数**: ファイルパスの正規化、一意なID(wpidex)の生成、タイムスタンプ取得など、様々な共通処理をサポートします。 このスクリプトは、ファイルパスの変更追跡、新規メディア情報の登録、JSONデータの自動生成と更新、さらには外部サービスとの連携を通じて、メディアライブラリの整合性を維持し、常に最新の状態に保つことを目指しています。 env終了 M1 Mac M2 Mac共通 バンドル:F1_メディアライブラリ GAS mediaLibary.gs トリガー: processWorkingRecord 1分毎 AppSheetの連携もあり、デプロイを行うこと。 デプロイ->デプロイを管理->編集->新バージョンを選択->デプロイ ``` // [メタ情報] // 識別子: マイライブラリ_生成更新処理_pub // システム名: マイライブラリ_生成更新処理 // 技術種別: GAS // 使用言語: JavaScript // 状態: 公開用 // 作成日: 2025年1月27日 (JST) // [/メタ情報] /** * ============================== * 📌 環境変数(スクリプトプロパティ)およびグローバル定数 * ============================== */ // ホームURLを動的解決するユーティリティ function getHomeUrl() { const endpoint = PropertiesService.getScriptProperties().getProperty("XSERVER_EMBED_ENDPOINT") || 'https://XXXXXX.com/update_videoembed.php'; if (endpoint) { const parts = endpoint.split(/\/update_videoembed\.php.*/i); if (parts.length > 0 && parts[0]) { let home = parts[0].trim(); if (!home.endsWith("/")) home += "/"; return home; } } return "https://XXXXXX.com/"; } // 保存用ライブラリJSONファイル名の解決 function getLibraryJsonName() { return PropertiesService.getScriptProperties().getProperty("LIBRARY_JSON_NAME") || "dropbox_wp_library.json"; } // 起動前提条件チェック function checkRequiredProperties() { const f1Id = PropertiesService.getScriptProperties().getProperty("F1_SPREADSHEET_ID"); const jsonId = PropertiesService.getScriptProperties().getProperty("JSON_FILE_ID"); if (!f1Id || !jsonId) { Logger.log("❌ [自律停止] 必要な鍵(F1_SPREADSHEET_ID または JSON_FILE_ID)が.env(スクリプトプロパティ)に設定されていません。"); return false; } return true; } /** * ============================== * 📌 ユーティリティ関数 (共通処理) * ============================== */ 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"); } /** * sb-付き一時ファイル名を本物に戻す */ function stripSbSuffix(path) { if (!path) return path; return path.replace(/(\.[A-Za-z0-9]+)\.sb-[A-Za-z0-9_-]+$/, "$1"); } /** (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 getHomeUrl() + "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; } /** * ============================== * 🔧 追加ユーティリティ(表記ゆれ吸収) * ============================== */ /** パス正規化 */ 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; } /** 正規化検索 */ 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; } 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) * ============================== */ function triggerVideoembedRebuild_() { try { if (typeof rebuildVideoembedJson === 'function') { Logger.log('🧩 trigger: rebuildVideoembedJson() を実行します'); const result = rebuildVideoembedJson(); return (result && result.sent !== false); } if (typeof exportVideoembedJson === 'function') { Logger.log('🧩 trigger: exportVideoembedJson()'); exportVideoembedJson(); if (typeof postVideoembedJsonToXserver_ === 'function') { Logger.log('🧩 trigger: postVideoembedJsonToXserver_()'); postVideoembedJsonToXserver_(); } return true; } if (typeof generateEmbedJSON === 'function') { Logger.log('🧩 trigger: generateEmbedJSON()'); generateEmbedJSON(); if (typeof postVideoembedJsonToXserver_ === 'function') { Logger.log('🧩 trigger: postVideoembedJsonToXserver_()'); postVideoembedJsonToXserver_(); } return true; } Logger.log('ℹ️ videoembed 再生成関数が有効ではありません。'); return false; } catch (err) { Logger.log('❌ triggerVideoembedRebuild_ エラー: ' + err); return false; } } /** * Webhook 受け口 */ function doPost(e) { Logger.log('🚀 doPost() 実行開始'); if (!checkRequiredProperties()) { return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: '必要な鍵(F1_SPREADSHEET_ID または JSON_FILE_ID)が.envに設定されていません。' })).setMimeType(ContentService.MimeType.JSON); } 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); } 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) フル再構築 ---- if (mode === 'FULL_REBUILD') { Logger.log('🔧 FULL_REBUILD → ライブラリ JSON 再生成 → 整頓 → videoembed 更新'); if (typeof updateJsonFile === 'function') updateJsonFile(); if (typeof cleanMediaLibrary === 'function') cleanMediaLibrary(); const ok = triggerVideoembedRebuild_(); return ContentService.createTextOutput(JSON.stringify({ status: ok ? 'success' : 'partial', mode })) .setMimeType(ContentService.MimeType.JSON); } const ss = SpreadsheetApp.openById(PropertiesService.getScriptProperties().getProperty("F1_SPREADSHEET_ID")); const sheet1 = ss.getSheetByName('sheet1'); let lastRow = sheet1.getLastRow(); let filePath = (data.filePath || '').trim(); filePath = stripSbSuffix(filePath); let dropboxLink = (data.dropboxLink || '').trim(); let fileExtn = (data.extn || '').trim(); let fileName = (filePath.split('/').pop() || '').trim() || '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); } let valuesB = (lastRow > 0) ? sheet1.getRange(1, 2, lastRow, 1).getValues() : []; let newWpidex = generateUniqueWpidex(fileExtn, valuesB); let mediaPath = extractMediaPath(filePath); let wpPathlinkUrl = getHomeUrl() + 'wp-content/' + mediaPath; let encodedUrl = encodeUrl(wpPathlinkUrl); const sheet2 = ss.getSheetByName('sheet2'); let selectNo = getSelectNo(fileExtn, sheet2); let currentTime = getCurrentTimestamp(); let newRow = lastRow + 1; sheet1.getRange(newRow, 1, 1, 14).setValues([[ dropboxLink, // A newWpidex, // B getHomeUrl() + 'rd.php?id=' + newWpidex, // C wpPathlinkUrl, // D encodedUrl, // E filePath, // F fileName, // G fileExtn, // H selectNo, // I currentTime, // J 'ADD', // K getHomeUrl() + 'rd.php?id=' + newWpidex, // L '', // M currentTime // N ]]); SpreadsheetApp.flush(); Logger.log('✅ 作業用レコード追加 - 行 ' + newRow); processWorkingRecord(); Logger.log('✅ processWorkingRecord() 呼び出し完了'); 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); } } function processWorkingRecord() { const lock = LockService.getScriptLock(); if (!lock.tryLock(0)) { Logger.log('⏭ processWorkingRecord: すでに実行中のためスキップ'); return; } if (!checkRequiredProperties()) { lock.releaseLock(); return; } try { Logger.log("🚀 processWorkingRecord() 実行開始"); const ss = SpreadsheetApp.openById(PropertiesService.getScriptProperties().getProperty("F1_SPREADSHEET_ID")); const sheet1 = ss.getSheetByName("sheet1"); const ssF3 = SpreadsheetApp.openById(PropertiesService.getScriptProperties().getProperty("F3_SPREADSHEET_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++) { const beforeRaw = safeTrim(f3Data[i][0] || ""); const afterRaw = safeTrim(f3Data[i][1] || ""); const flagRaw = f3Data[i][3]; const flag = String(flagRaw).toUpperCase(); if (flag !== "TRUE") continue; if (!beforeRaw || !afterRaw) continue; const beforePath = normalizePath(stripSbSuffix(beforeRaw)); const afterPath = normalizePath(stripSbSuffix(afterRaw)); const f1Row = findRowByNormalizedPath(f1Data, 6, beforePath); if (f1Row > 0) { const newFileName = baseNameFromPath(afterPath); const extn = newFileName.includes(".") ? newFileName.split(".").pop() : ""; const selectNo = getSelectNo(extn, ss.getSheetByName("sheet2")); const wpPathlinkUrl = generateWpPathlinkUrl(afterPath); const encodedUrl = encodeUrl(wpPathlinkUrl); Logger.log(`🟢 F3適用: F1行 ${f1Row} を更新 ${beforeRaw} → ${afterRaw}(sb除外後: ${beforePath} → ${afterPath})`); sheet1.getRange(f1Row, 4).setValue(wpPathlinkUrl); // D sheet1.getRange(f1Row, 5).setValue(encodedUrl); // E sheet1.getRange(f1Row, 6).setValue(afterPath); // F sheet1.getRange(f1Row, 7).setValue(newFileName); // G sheet1.getRange(f1Row, 8).setValue(extn); // H sheet1.getRange(f1Row, 9).setValue(selectNo); // I sheet1.getRange(f1Row, 10).setValue(getCurrentTimestamp()); // J if (typeof runTwinRename === 'function') { runTwinRename(f1Data, f1Row, newFileName, sheet1); } sheetF3.getRange(i + 1, 4).setValue("FALSE"); updateRequired = true; } else { Logger.log(`⚠️ F3適用スキップ: 旧パスが F1 に見当たりません → ${beforeRaw}`); sheetF3.getRange(i + 1, 4).setValue("FALSE"); } } } if (updateRequired) { SpreadsheetApp.flush(); Utilities.sleep(300); f1Data = sheet1.getDataRange().getValues(); lastRow = f1Data.length; } // ========================= B) K=ADD の処理 ========================= for (let i = 1; i < lastRow; i++) { if (f1Data[i][10] !== "ADD") continue; let filePath = safeTrim(f1Data[i][5]); filePath = stripSbSuffix(filePath); let dropboxLink = safeTrim(f1Data[i][0]); let rowNum = i + 1; Logger.log(`🟢 (1) 処理開始 - 行: ${rowNum}, ファイルパス: ${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}`); sheet1.getRange(rowNum, 6).setValue(f3AValue); filePath = f3AValue; } else { Logger.log("ℹ️ (1) F3(B) に一致なし"); } f3Row = findRowInColumnExcludingNormalized(f3Data, 1, filePath, [1]); if (f3Row > 0) { let f3BValue = safeTrim(f3Data[f3Row - 1][1]); Logger.log(`✅ (2) F3(A) に一致 - F3(B): ${f3BValue}`); sheet1.getRange(rowNum, 6).setValue(f3BValue); filePath = f3BValue; } else { Logger.log("ℹ️ (2) F3(A) に一致なし"); } filePath = safeTrim(sheet1.getRange(rowNum, 6).getValue()); if (!dropboxLink) { dropboxLink = safeTrim(f1Data[i][0]); } if (!filePath) { Logger.log(`⚠️ (3) F列ファイルパスが空のためスキップ - 行: ${rowNum}`); continue; } const fileName = baseNameFromPath(filePath); const extn = fileName.includes(".") ? fileName.split(".").pop() : ""; const selectNo = getSelectNo(extn, ss.getSheetByName("sheet2")); const wpPathlinkUrl = generateWpPathlinkUrl(filePath); const encodedUrl = encodeUrl(wpPathlinkUrl); const ts = getCurrentTimestamp(); let existingRowA = findRowInColumnExcludingNormalized(f1Data, 6, filePath, [rowNum]); let dropboxExists = findRowInColumnExcludingNormalized(f1Data, 1, dropboxLink, [rowNum]); let filePathNotExists = (existingRowA === 0); Logger.log(`🔎 状態: F列(F) 既存: ${!filePathNotExists}, dropboxLink 一致: ${dropboxExists > 0}`); if (!filePathNotExists) { Logger.log(`✅ (4) 既存レコード更新 - 行 ${existingRowA}`); if (dropboxLink) sheet1.getRange(existingRowA, 1).setValue(dropboxLink); sheet1.getRange(existingRowA, 4).setValue(wpPathlinkUrl); sheet1.getRange(existingRowA, 5).setValue(encodedUrl); sheet1.getRange(existingRowA, 6).setValue(filePath); sheet1.getRange(existingRowA, 7).setValue(fileName); sheet1.getRange(existingRowA, 8).setValue(extn); sheet1.getRange(existingRowA, 9).setValue(selectNo); sheet1.getRange(existingRowA, 10).setValue(ts); sheet1.getRange(existingRowA, 11).setValue("CHG"); sheet1.getRange(rowNum, 11).setValue("DEL"); updateRequired = true; } else { Logger.log(`✅ (5) 新規候補 → 作業用行を ADD2 に変更`); sheet1.getRange(rowNum, 1).setValue(dropboxLink); sheet1.getRange(rowNum, 4).setValue(wpPathlinkUrl); sheet1.getRange(rowNum, 5).setValue(encodedUrl); sheet1.getRange(rowNum, 6).setValue(filePath); sheet1.getRange(rowNum, 7).setValue(fileName); sheet1.getRange(rowNum, 8).setValue(extn); sheet1.getRange(rowNum, 9).setValue(selectNo); sheet1.getRange(rowNum, 10).setValue(ts); sheet1.getRange(rowNum, 11).setValue("ADD2"); updateRequired = true; } } // ========================= C) K列の検知・連動処理 ========================= for (let i = 1; i < lastRow; i++) { const kStatus = f1Data[i][10]; if (kStatus === "CHG" || kStatus === "ADD2" || kStatus === "DEL") { updateRequired = true; break; } } if (!updateRequired) { Logger.log("ℹ️ 更新対象なし → 終了"); } else { Logger.log("✅ processWorkingRecord() 正常終了(更新あり)"); SpreadsheetApp.flush(); Utilities.sleep(15000); // 1) LIBRARY_JSON_NAME の更新 try { Logger.log(`🔄 JSON ファイル(${getLibraryJsonName()})の更新を開始します`); updateJsonFile(); Logger.log(`✅ JSON ファイルの更新が完了しました`); } catch (error) { Logger.log(`❌ JSON 更新エラー: ${error.message}`); } // 2) ライブラリ整理 try { if (typeof cleanMediaLibrary === 'function') { Logger.log("🧹 メディアライブラリの整理を開始します"); cleanMediaLibrary(); Logger.log("✅ メディアライブラリの整理が完了しました"); } else { Logger.log("ℹ️ cleanMediaLibrary() が定義されていないためスキップ"); } } catch (error) { Logger.log(`❌ cleanMediaLibrary() エラー: ${error.message}`); } // 3) F1 → pcloudid 同期 try { Logger.log("🔁 F1 → pcloudid 同期を実行"); syncPcloudIdFromF1(); Logger.log("✅ F1 → pcloudid 同期完了"); } catch (error) { Logger.log(`❌ syncPcloudIdFromF1() エラー: ${error.message}`); } } } finally { lock.releaseLock(); } } /** * ============================== * 📌 テスト関数 (testDoPost) * ============================== */ function testDoPost() { Logger.log("🚀 testDoPost() 実行開始"); let testData = { dropboxLink: "https://www.dropbox.com/scl/fi//250307_.png?rlkey=&raw=1", filePath: "/Volumes/NO3_SSD/mybox/mybox_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(PropertiesService.getScriptProperties().getProperty("F1_SPREADSHEET_ID")); const sheet1 = ss.getSheetByName("sheet1"); let lastRow = sheet1.getLastRow(); if (lastRow === 0) { Logger.log("⚠️ シート空 → 中止"); return; } let f1Data = sheet1.getRange(1, 1, lastRow, 21).getValues(); Logger.log(`📜 データ取得 - 行数: ${f1Data.length}`); let jsonData = []; let hasRow = false; for (let i = 1; i < f1Data.length; i++) { if (f1Data[i][10] === "DEL") continue; 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], column_N: f1Data[i][13], column_O: f1Data[i][14], column_P: f1Data[i][15], bn_low_wpidex: f1Data[i][18] || "", bn_high_url: f1Data[i][19] || "", bn_low_url: f1Data[i][20] || "" }); } if (!hasRow) { Logger.log("⚠️ JSON対象行なし(DEL以外が0): 書き込み中止"); return; } const jsonString = JSON.stringify(jsonData, null, 2); const FILE_ID = PropertiesService.getScriptProperties().getProperty("JSON_FILE_ID"); 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()); 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(PropertiesService.getScriptProperties().getProperty("F1_SPREADSHEET_ID")); 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) 重複除去 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) { row[9] = existing[9]; row[13] = existing[13]; for (let c = 12; c <= 20; c++) { if ((row[c] === "" || row[c] == null) && existing[c] !== "") { row[c] = existing[c]; } } uniqueMap.set(key, row); } else { existing[9] = row[9]; existing[13] = row[13]; for (let c = 12; c <= 20; c++) { if ((existing[c] === "" || existing[c] == null) && row[c] !== "") { existing[c] = row[c]; } } uniqueMap.set(key, existing); } } }); newData = Array.from(uniqueMap.values()); // 5) J列(更新日時)降順ソート newData.sort((a, b) => new Date(b[9]) - new Date(a[9])); // 6) ★超安全書き戻し処理(2重の安全ガード) try { if (newData.length > 0) { const lastRowOnSheet = sheet1.getLastRow(); // シート全体のクリア(clearContents)を廃止! // 代わりに、2行目以降の「古いデータ部分のみ」を消去。 // これにより、万が一エラーが起きてもヘッダー行(1行目)は絶対に保護されます。 if (lastRowOnSheet > 1) { sheet1.getRange(2, 1, lastRowOnSheet - 1, sheet1.getLastColumn()).clearContent(); } // 新しいデータを書き戻す sheet1.getRange(2, 1, newData.length, newData[0].length).setValues(newData); Logger.log("✅ メディアライブラリの整理が正常に完了しました(2行目以降のみ再書き込み)"); } else { // ⚠️ 安全ガード:万が一整理後のデータが0件と判定された場合、 // 既存シートのデータを守るためにクリア処理を絶対に実行せず安全に終了します。 Logger.log("⚠️ [安全ガード作動] 整理後のデータが0件のため、既存シートのクリアおよび書き戻しを完全にスキップしました。データを保護しました。"); } } catch (err) { Logger.log("❌ [書き戻しエラー] データの書き戻し中に問題が発生したため、書き込みを完全にロールバックして中断しました: " + err.message); } } /***** videoembed.json 送信用の設定 *****/ const VIDEOEMBED_SS_ID = PropertiesService.getScriptProperties().getProperty("F1_SPREADSHEET_ID"); const VIDEOEMBED_SHEET = '動画パッケージ'; const VIDEOEMBED_COLS = { videoid: 0, embed: 1 }; function getXserverEmbedEndpoint() { return PropertiesService.getScriptProperties().getProperty("XSERVER_EMBED_ENDPOINT") || 'https://XXXXXX.com/update_videoembed.php'; } function getXserverEmbedToken() { return PropertiesService.getScriptProperties().getProperty("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, embedCode: embed }); } Logger.log(`🧰 videoembed payload rows=${out.length}`); return out; } /** Xserver の update_videoembed.php に POST */ function postVideoembedJsonToXserver_() { const endpoint = getXserverEmbedEndpoint(); if (endpoint.toLowerCase().includes('.disabled')) { Logger.log('⚠️ [通信スキップ] Xserver送信先エンドポイントが無効化されています (.DISABLED)。連携をスキップします。'); return { sent: false, count: 0, disabled: true }; } const payload = buildVideoembedPayload_(); if (payload.length === 0) { Logger.log('⚠️ videoembed: 送信0件のため中断'); return { sent: false, count: 0 }; } const token = getXserverEmbedToken(); if (!token) { Logger.log('⚠️ [通信スキップ] XSERVER_EMBED_TOKEN がスクリプトプロパティに設定されていません。'); return { sent: false, count: 0 }; } const url = endpoint + '?token=' + encodeURIComponent(token); try { 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) { Logger.log(`⚠️ videoembed POST 警告応答コード: ${res.getResponseCode()}`); return { sent: false, count: payload.length, error: true }; } return { sent: true, count: payload.length }; } catch (err) { Logger.log(`❌ videoembed POST 通信例外(スキップします): ${err}`); return { sent: false, count: payload.length, error: true }; } } /** doPost() から呼べる“統一口” */ function rebuildVideoembedJson() { return postVideoembedJsonToXserver_(); } /** * ============================================ * F1(sheet1) → pcloudid 同期 * ============================================ */ function syncPcloudIdFromF1() { const ss = SpreadsheetApp.openById(PropertiesService.getScriptProperties().getProperty("F1_SPREADSHEET_ID")); const f1 = ss.getSheetByName('sheet1'); const pc = ss.getSheetByName('pcloudid'); if (!f1 || !pc) { Logger.log('⚠ sheet1 または pcloudid シートが見つかりません'); return; } const f1LastRow = f1.getLastRow(); const f1LastCol = f1.getLastColumn(); if (f1LastRow < 2) { Logger.log('sheet1 にデータ行がありません'); return; } const f1Values = f1.getRange(2, 1, f1LastRow - 1, f1LastCol).getValues(); const COL_WPIDEX = 2; const COL_FILEPATH = 6; const COL_FILENAME = 7; const f1Map = {}; f1Values.forEach(row => { const wpidex = safeTrim(row[COL_WPIDEX - 1]); const filename = safeTrim(row[COL_FILENAME - 1]); const fullPath = safeTrim(row[COL_FILEPATH - 1]); if (!wpidex) return; const relPath = extractPcloudRelativePath_(fullPath); f1Map[wpidex] = { filename, relPath }; }); const pcLastRow = pc.getLastRow(); const pcCols = 7; const pcValues = pcLastRow > 1 ? pc.getRange(2, 1, pcLastRow - 1, pcCols).getValues() : []; const pcIndexByWpidex = new Map(); pcValues.forEach((row, idx) => { const wpidex = safeTrim(row[0]); if (wpidex) pcIndexByWpidex.set(wpidex, idx); }); const now = new Date(); let changed = false; Object.keys(f1Map).forEach(wpidex => { const { filename, relPath } = f1Map[wpidex]; const idx = pcIndexByWpidex.get(wpidex); if (idx == null) { pcValues.push([ wpidex, '', filename || '', relPath || '', '', true, now ]); pcIndexByWpidex.set(wpidex, pcValues.length - 1); changed = true; Logger.log(`➕ pcloudid に新規追加: wpidex=${wpidex}, relPath=${relPath}`); } else { const row = pcValues[idx]; const oldFilename = safeTrim(row[2]); const oldRelPath = safeTrim(row[3]); const isFilenameChanged = filename && filename !== oldFilename; const isPathChanged = relPath && relPath !== oldRelPath; if (isFilenameChanged || isPathChanged) { if (isFilenameChanged) row[2] = filename; if (isPathChanged) row[3] = relPath; row[5] = true; row[6] = now; changed = true; Logger.log( `✏️ pcloudid 更新: wpidex=${wpidex}, filename="${oldFilename}"→"${filename}", filepath="${oldRelPath}"→"${relPath}"` ); } } }); if (!changed) { Logger.log('ℹ syncPcloudIdFromF1: 反映すべき変更はありません'); return; } if (pcValues.length > 0) { pc.getRange(2, 1, pcValues.length, pcCols).setValues(pcValues); } Logger.log(`✅ syncPcloudIdFromF1 完了。行数=${pcValues.length}`); } function extractPcloudRelativePath_(fullPath) { const p = safeTrim(fullPath); if (!p) return ''; const mIdx = p.indexOf('/mmedia/'); const pIdx = p.indexOf('/pmedia/'); let idx = -1; if (mIdx >= 0 && pIdx >= 0) { idx = Math.min(mIdx, pIdx); } else if (mIdx >= 0) { idx = mIdx; } else if (pIdx >= 0) { idx = pIdx; } if (idx < 0) { return p; } return p.substring(idx); } function forceUpdateTest() { Logger.log("🚀 強制更新テスト開始"); updateJsonFile(); Logger.log("✅ 強制更新テスト終了"); } ```