# [メタ情報] # 識別子: マイライブラリ_生成更新処理_exe # システム名: マイライブラリ_生成更新処理 # 技術種別: Misc # 機能名: Misc # 使用言語: GAS # 状態: 実行用 # [/メタ情報] 要約:mediaLibrary.gsは、Dropbox等のメディアファイル情報をF1シートで一元管理し、その変更を自動反映するGASです。ファイル名・パスから一意なwpidexを生成し、sb付き一時ファイル名や?v=付きURL、NFC差、連続スラッシュなどを正規化して突合します。doPostはmodeにより、ライブラリのフル再構築やvideoembed.jsonのみ更新、通常の作業行追加+処理を切り替えます。processWorkingRecordはF3のパス変更履歴を反映しつつ、K=ADD行をADD2に更新し、JSON生成とライブラリ整理、pcloudidシートへの同期を行います。cleanMediaLibraryはDEL行や重複を除去し、更新日時で並べ替え、フラグをFALSEに戻します。videoembed関連は「動画パッケージ」シートから埋め込みHTMLを集約し、XserverのPHPにJSONをPOST送信します。 M1 Mac M2 Mac共通 バンドル:F1_メディアライブラリ GAS mediaLibary.gs トリガー: processWorkingRecord 1分毎 /** * ============================== * 📌 mediaLibrary.gs(安定版 + sb対策) * ============================== */ /** * ============================== * 📌 ユーティリティ関数 (共通処理) * ============================== */ 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-付き一時ファイル名を本物に戻す * 例: ".../説明.txt.sb-6feb7b48-U0KQ6" → ".../説明.txt" */ 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 "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(); filePath = stripSbSuffix(filePath); // ★ sb一時ファイル名を本物に戻す 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 更新 → ライブラリ整理 → pcloudid 同期 * ============================== */ function processWorkingRecord() { // ★ ここからロック追加部分 ★ const lock = LockService.getScriptLock(); if (!lock.tryLock(0)) { Logger.log('⏭ processWorkingRecord: すでに実行中のためスキップ'); return; } try { 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 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; // sb- や .sb-xxxxx を除去しつつ正規化 const beforePath = normalizePath(stripSbSuffix(beforeRaw)); const afterPath = normalizePath(stripSbSuffix(afterRaw)); // 旧パスに一致する F1 行(F列=6)を正規化比較で検索(完全一致) const f1Row = findRowByNormalizedPath(f1Data, 6, beforePath); if (f1Row > 0) { const newFileName = baseNameFromPath(afterPath); const extn = newFileName.includes(".") ? newFileName.split(".").pop() : ""; const wpPathlinkUrl = generateWpPathlinkUrl(afterPath); const encodedUrl = encodeUrl(wpPathlinkUrl); Logger.log( `🟢 F3適用: F1行 ${f1Row} を更新 ${beforeRaw} → ${afterRaw}(sb除外後: ${beforePath} → ${afterPath})` ); // B(wpidex) は触らない。D/E/F/G/H/J/K のみ更新 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,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 に見当たりません → ${beforeRaw}`); } } } 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); // ← ★必ずここで sb を除去 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}`); sheet1.getRange(rowNum, 6).setValue(f3AValue); filePath = f3AValue; } else { Logger.log("ℹ️ (1) F3(B) に一致なし"); } // (2)F3(A) に filePath が存在するか(正規化版) 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 は最新の状態 filePath = safeTrim(sheet1.getRange(rowNum, 6).getValue()); // もし A(Dropboxリンク)が空なら、既存のものを使う if (!dropboxLink) { dropboxLink = safeTrim(f1Data[i][0]); } // F列ファイルパスが空ならスキップ if (!filePath) { Logger.log(`⚠️ (3) F列ファイルパスが空のためスキップ - 行: ${rowNum}`); continue; } // 拡張子を抽出して select_no を取得 const fileName = baseNameFromPath(filePath); const extn = fileName.includes(".") ? fileName.split(".").pop() : ""; const selectNo = getSelectNo(extn, ss.getSheetByName("sheet2")); // mmedia/pmedia 判定 const mediaType = extractMediaCategory(filePath); // wp_pathlink_url, wp_encoded_url を生成 const wpPathlinkUrl = generateWpPathlinkUrl(filePath); const encodedUrl = encodeUrl(wpPathlinkUrl); // F1 への書き戻し sheet1.getRange(rowNum, 1).setValue(dropboxLink); // A: dropbox_link sheet1.getRange(rowNum, 4).setValue(wpPathlinkUrl); // D: wp_pathlink_url sheet1.getRange(rowNum, 5).setValue(encodedUrl); // E: wp_encoded_url sheet1.getRange(rowNum, 6).setValue(filePath); // F: file_path sheet1.getRange(rowNum, 7).setValue(fileName); // G: file_name sheet1.getRange(rowNum, 8).setValue(extn); // H: extn sheet1.getRange(rowNum, 9).setValue(mediaType); // I: media_type sheet1.getRange(rowNum,10).setValue(getCurrentTimestamp()); // J: updated_at sheet1.getRange(rowNum,11).setValue("ADD2"); // K: flag Logger.log(`✅ (4) F1 更新完了 - 行: ${rowNum}`); updateRequired = true; } if (!updateRequired) { Logger.log("ℹ️ 更新対象なし → 終了"); } else { Logger.log("✅ processWorkingRecord() 正常終了(更新あり)"); // --- ここから JSON 更新 & ライブラリ整理 & pcloudid 同期を追加 --- SpreadsheetApp.flush(); Utilities.sleep(2000); // シート反映待ち(軽めのウェイト) // 1) dropbox_wp_library.json の更新 try { Logger.log("🔄 JSON ファイル(dropbox_wp_library.json)の更新を開始します"); updateJsonFile(); Logger.log("✅ JSON ファイルの更新が完了しました"); } catch (error) { Logger.log(`❌ JSON 更新エラー: ${error.message}`); } // 2) ライブラリ整理(K列を FALSE に戻すなど) try { if (typeof cleanMediaLibrary === 'function') { Logger.log("🧹 メディアライブラリの整理を開始します"); cleanMediaLibrary(); Logger.log("✅ メディアライブラリの整理が完了しました"); } else { Logger.log("ℹ️ cleanMediaLibrary() が定義されていないためスキップ"); } } catch (error) { Logger.log(`❌ cleanMediaLibrary() エラー: ${error.message}`); } // 3) F1 → pcloudid 同期(pCloud 側の更新フラグを立てる) 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/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_(); } /** * ============================================ * F1(sheet1) → pcloudid 同期(列位置固定版) * - wpidex をキーに、filename / filepath を反映 * - 変更があれば pcloudid.needs_update を TRUE にする * - pcloudid に存在しない wpidex は新規行として追加 * * 前提: * - F1(sheet1) の列構成: * B: wpidex * F: file_path(ローカルフルパス) * G: file_name * - pcloudid のレイアウト: * A: wpidex * B: fileid * C: filename * D: filepath (/mmedia/... or /pmedia/...) * E: direct_url * F: needs_update * G: last_update * ============================================ */ function syncPcloudIdFromF1() { // ★ F1 ブックを ID で明示 const ss = SpreadsheetApp.openById('<あなたのspreadsheetID>'); 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; } // F1 全行取得 const f1Values = f1.getRange(2, 1, f1LastRow - 1, f1LastCol).getValues(); // 列位置(1始まり) const COL_WPIDEX = 2; // B const COL_FILEPATH = 6; // F const COL_FILENAME = 7; // G // wpidex → { filename, relPath } のマップを作成 /** @type {Object} */ 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); // /mmedia/... or /pmedia/... f1Map[wpidex] = { filename, relPath }; }); // ---- pcloudid 側を読み込み ---- const pcLastRow = pc.getLastRow(); const pcCols = 7; // A:G /** 2行目以降の既存行 */ const pcValues = pcLastRow > 1 ? pc.getRange(2, 1, pcLastRow - 1, pcCols).getValues() : []; // 既存行の wpidex → index マップ const pcIndexByWpidex = new Map(); pcValues.forEach((row, idx) => { const wpidex = safeTrim(row[0]); // A if (wpidex) pcIndexByWpidex.set(wpidex, idx); }); const now = new Date(); let changed = false; // ---- F1 の情報を pcloudid に反映 ---- Object.keys(f1Map).forEach(wpidex => { const { filename, relPath } = f1Map[wpidex]; const idx = pcIndexByWpidex.get(wpidex); if (idx == null) { // ★ 新規 wpidex → 末尾に追加 pcValues.push([ wpidex, // A: wpidex '', // B: fileid(まだ不明) filename || '',// C: filename relPath || '', // D: filepath (/mmedia/..., /pmedia/...) '', // E: direct_url true, // F: needs_update now // G: last_update ]); 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]); // C const oldRelPath = safeTrim(row[3]); // D const isFilenameChanged = filename && filename !== oldFilename; const isPathChanged = relPath && relPath !== oldRelPath; if (isFilenameChanged || isPathChanged) { if (isFilenameChanged) row[2] = filename; if (isPathChanged) row[3] = relPath; // fileid はそのまま・direct_url は updatePcloudLibrary が更新する row[5] = true; // F: needs_update row[6] = now; // G: last_update changed = true; Logger.log( `✏️ pcloudid 更新: wpidex=${wpidex}, filename="${oldFilename}"→"${filename}", filepath="${oldRelPath}"→"${relPath}"` ); } } }); if (!changed) { Logger.log('ℹ syncPcloudIdFromF1: 反映すべき変更はありません'); return; } // ---- pcloudid に書き戻し ---- if (pcValues.length > 0) { pc.getRange(2, 1, pcValues.length, pcCols).setValues(pcValues); } Logger.log(`✅ syncPcloudIdFromF1 完了。行数=${pcValues.length}`); } /** * ローカルフルパスから、pCloud 用の相対パス (/mmedia/... or /pmedia/...) を抽出 * 例: * "/Volumes/NO3_SSD/Dropbox/dropbox_1/mmedia/aaa/bbb.png" * → "/mmedia/aaa/bbb.png" */ 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) { // mmedia/pmedia を含まないパスはそのまま返す(保険) return p; } return p.substring(idx); // 先頭から "/mmedia/..." または "/pmedia/..." 部分 }