# [メタ情報] # 識別子: pCloud用再生URLおよびJSON_生成_exe # システム名: pCloud用再生URLおよびJSON_生成 # 技術種別: Misc # 機能名: Misc # 使用言語: GAS # 状態: 実行用 # [/メタ情報] 要約:このスクリプトは、Googleスプレッドシートの「pcloudid」と「sheet1」を基に、pCloud上のファイル情報を自動更新し、filedn形式の直接URLを生成・管理するGASです。sheet1に新規追加された未登録のwpidexをpcloudidへ自動追加し、needs_updateがTRUEの行のみを対象に、filepathやfileidからpCloud APIを用いてメタ情報とパスを解決します。生成したdirect_urlや更新日時を反映し、最終的に全データをlast_update順に整列したうえで、pcloud_library.jsonをGoogle Drive上の固定IDファイルへ上書き出力します。 updatePcloudLibrary.gs ``` /** * ============================================ * pCloud 用ライブラリ更新スクリプト(filedn 版) * 【2026/01/29 画像による列位置修正版】 * * - シート: pcloudid * A: wpidex * B: fileid (pCloud ID) * C: filename (ファイル名) * D: filepath (ファイルパス /mmedia/...) ★ここを読み取る * E: direct_url * F: needs_update * G: last_update * H: ID (予備ID列。ここにもpCloud IDを表示させる) * ============================================ */ /** pcloudid シート名 */ const PCLOUD_SHEET_NAME = 'pcloudid'; /** sheet1(メディアライブラリ)シート名 */ const SOURCE_SHEET1_NAME = 'sheet1'; /** Drive に書き出す JSON ファイル名 */ const PCLOUD_JSON_FILENAME = 'pcloud_library.json'; /** Drive 上の pcloud_library.json のファイルID */ const PCLOUD_JSON_FILE_ID = '<あなたのID>'; /* API設定 */ const PCLOUD_API_BASE = 'https://eapi.pcloud.com'; const PCLOUD_FILEDN_BASE = 'https://filedn.eu/<あなたのpCloudトークン>/'; const PCLOUD_PUBLIC_ROOT_NAME = 'Public Folder'; /* NFD正規化 */ function normalizeToNFD_(s) { if (!s) return s; try { if (typeof s.normalize === 'function') return s.normalize('NFD'); } catch (e) {} return s; } /** テスト用関数 */ function testPcloudSetup() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(PCLOUD_SHEET_NAME); if (!sheet) { Logger.log('⚠ pcloudid シートが見つかりません'); return; } Logger.log('✅ pcloudid シート確認OK。最終行: ' + sheet.getLastRow()); try { const auth = getPcloudAuthToken_(); if (auth) Logger.log('✅ pCloudログイン成功'); } catch(e) { Logger.log('❌ pCloudログイン失敗: ' + e); } } /** * ============================================ * メイン処理 * ============================================ */ function updatePcloudLibrary() { const ss = SpreadsheetApp.getActiveSpreadsheet(); // 1. 新規レコードの同期 (sheet1 -> pcloudid) try { syncNewRecordsFromSheet1ToPcloudid_(); } catch (e) { Logger.log('⚠ 新規同期エラー: ' + e); } const sheet = ss.getSheetByName(PCLOUD_SHEET_NAME); if (!sheet) return; let authToken; try { authToken = getPcloudAuthToken_(); } catch (e) { Logger.log('❌ トークン取得エラー: ' + e); return; } const lastRow = sheet.getLastRow(); if (lastRow < 2) { writePcloudLibraryJson_([]); return; } // ★変更: A〜H列 (8列分) を取得 const range = sheet.getRange(2, 1, lastRow - 1, 8); const values = range.getValues(); const now = new Date(); let sheetChanged = false; const folderPathCache = {}; values.forEach((row, idx) => { // --- 列マッピング (0始まり) --- const wpidex = String(row[0]).trim(); // A: wpidex let fileidB = String(row[1]).trim(); // B: fileid let filename = String(row[2]).trim(); // C: filename const filepath = String(row[3]).trim(); // D: filepath (★ここが重要) let direct = String(row[4]).trim(); // E: direct_url let needUpd = row[5]; // F: needs_update let lastUpd = row[6]; // G: last_update let fileidH = String(row[7]).trim(); // H: ID if (!wpidex) return; // 空行スキップ // IDの決定 (B列優先、なければH列) let fileid = fileidB; // もしB列が空でH列が数字ならH列を採用(あまりないケースだが念のため) if (!fileid && fileidH && /^\d+$/.test(fileidH)) { fileid = fileidH; } // ---------------------------------------------------- // 1) fileid が空で filepath がある場合 → fileid を取得 // ---------------------------------------------------- if (!fileid && filepath) { try { const info = fetchFileidFromLocalFilepath_(filepath, authToken); fileid = String(info.fileid); // メタデータからファイル名も更新しておく if (!filename) filename = filepath.split('/').pop(); // 配列データを更新 row[1] = fileid; // B列 row[2] = filename; // C列 row[7] = fileid; // H列にも同じIDを入れる sheetChanged = true; Logger.log('✅ ID取得成功: ' + filepath + ' -> ' + fileid); } catch (e) { // IDが取れない場合はスキップ return; } } // fileid が無いと以降の処理はできない if (!fileid) return; // needs_update チェック const needUpdateBool = needUpd === true || (typeof needUpd === 'string' && needUpd.toUpperCase() === 'TRUE') || needUpd === 1; // H列(ID)が変な数値(0.xxxx)だったり空だったりしたら、B列(正)と同期させる if (row[7] !== fileid) { row[7] = fileid; sheetChanged = true; } if (!needUpdateBool) { return; // 更新不要 } // ---------------------------------------------------- // 2) 更新処理 (URL生成) // ---------------------------------------------------- try { const meta = fetchPcloudFileMetaByFileid_(fileid, authToken); const pcloudPath = buildPcloudPathFromMeta_(meta, authToken, folderPathCache); const directUrl = buildFilednUrlFromPcloudPath_(pcloudPath); // 値更新 direct = directUrl; lastUpd = now; needUpd = false; if (!filename && meta.name) filename = meta.name; row[2] = filename; // C: filename row[4] = direct; // E: direct_url row[5] = false; // F: needs_update row[6] = lastUpd; // G: last_update row[1] = fileid; // B: fileid (念のため) row[7] = fileid; // H: ID (念のため) sheetChanged = true; Logger.log('✅ URL更新完了: ' + wpidex); } catch (e) { Logger.log('❌ 更新エラー: wpidex=' + wpidex + ', error=' + e); } }); // 3) ソート & 書き戻し sortByLastUpdateDesc_(values); range.setValues(values); // 4) JSON作成 writePcloudLibraryJson_(values); Logger.log('ℹ 処理完了。変更あり=' + sheetChanged); } /* =========================== * API & ユーティリティ * =========================== */ function getPcloudAuthToken_() { const props = PropertiesService.getScriptProperties(); const email = props.getProperty('PCLOUD_EMAIL'); const pass = props.getProperty('PCLOUD_PASSWORD'); if (!email || !pass) throw new Error('プロパティ未設定'); const url = PCLOUD_API_BASE + '/userinfo'; const res = UrlFetchApp.fetch(url, { method: 'post', payload: { getauth: 1, username: email, password: pass }, muteHttpExceptions: true }); const json = JSON.parse(res.getContentText()); if (json.result !== 0 || !json.auth) throw new Error('Auth取得失敗'); return json.auth; } function fetchFileidFromLocalFilepath_(filepath, authToken) { const pcloudPath = buildPcloudPathFromLocalFilepath_(filepath); const url = PCLOUD_API_BASE + '/stat?path=' + encodeURIComponent(pcloudPath) + '&auth=' + encodeURIComponent(authToken); const res = UrlFetchApp.fetch(url, { method: 'get', muteHttpExceptions: true }); const json = JSON.parse(res.getContentText()); if (json.result !== 0 || !json.metadata || !json.metadata.fileid) throw new Error('File not found: ' + pcloudPath); return { fileid: json.metadata.fileid, pcloudPath: pcloudPath }; } function fetchPcloudFileMetaByFileid_(fileid, authToken) { const url = PCLOUD_API_BASE + '/stat?fileid=' + encodeURIComponent(fileid) + '&auth=' + encodeURIComponent(authToken); const res = UrlFetchApp.fetch(url, { method: 'get', muteHttpExceptions: true }); const json = JSON.parse(res.getContentText()); if (json.result !== 0 || !json.metadata) throw new Error('stat error'); return json.metadata; } function resolvePcloudFolderPath_(folderId, authToken, cache) { if (!cache) cache = {}; if (folderId == 0) return '/'; const key = String(folderId); if (cache[key]) return cache[key]; const url = PCLOUD_API_BASE + '/stat?folderid=' + encodeURIComponent(folderId) + '&auth=' + encodeURIComponent(authToken); const res = UrlFetchApp.fetch(url, { method: 'get', muteHttpExceptions: true }); const json = JSON.parse(res.getContentText()); if (json.result !== 0 || !json.metadata) throw new Error('folder stat error'); const meta = json.metadata; const parentPath = resolvePcloudFolderPath_(meta.parentfolderid, authToken, cache); const name = meta.name || ''; const fullPath = (parentPath === '/') ? '/' + name : parentPath + '/' + name; cache[key] = fullPath; return fullPath; } function buildPcloudPathFromMeta_(meta, authToken, folderPathCache) { const name = meta.name || ''; const folderPath = resolvePcloudFolderPath_(meta.parentfolderid, authToken, folderPathCache); return (folderPath === '/') ? '/' + name : folderPath + '/' + name; } function buildPcloudPathFromLocalFilepath_(filepath) { if (!filepath) throw new Error('filepath empty'); let rel = filepath.trim(); if (!rel.startsWith('/')) rel = '/' + rel; return normalizeToNFD_('/' + PCLOUD_PUBLIC_ROOT_NAME + rel); } function buildFilednUrlFromPcloudPath_(pcloudPath) { if (!pcloudPath) throw new Error('path empty'); let relPath = pcloudPath.startsWith('/') ? pcloudPath.substring(1) : pcloudPath; const prefix = PCLOUD_PUBLIC_ROOT_NAME + '/'; if (relPath.startsWith(prefix)) relPath = relPath.substring(prefix.length); relPath = relPath.replace(/^\/+/, ''); const encodedPath = encodeURI(relPath); let base = PCLOUD_FILEDN_BASE; if (!base.endsWith('/')) base += '/'; return base + encodedPath; } /* =========================== * JSON / ソート / 新規追加 * =========================== */ function toMillis_(v) { if (v instanceof Date) return v.getTime(); return 0; } function sortByLastUpdateDesc_(values) { values.sort((a, b) => toMillis_(b[6]) - toMillis_(a[6])); } function writePcloudLibraryJson_(values) { const dataArr = []; values.forEach(row => { // IDは B列 or H列 const fileid = String(row[1]).trim() || String(row[7]).trim(); if (!row[0] || !fileid || !row[4]) return; dataArr.push({ wpidex: String(row[0]).trim(), fileid: fileid, direct_url: String(row[4]).trim(), last_update: row[6] ? Utilities.formatDate(new Date(row[6]), 'Asia/Tokyo', "yyyy-MM-dd'T'HH:mm:ssXXX") : null }); }); const jsonText = JSON.stringify(dataArr, null, 2); try { DriveApp.getFileById(PCLOUD_JSON_FILE_ID).setContent(jsonText); } catch (e) { DriveApp.createFile(PCLOUD_JSON_FILENAME, jsonText, MimeType.PLAIN_TEXT); } } function syncNewRecordsFromSheet1ToPcloudid_() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const src = ss.getSheetByName(SOURCE_SHEET1_NAME); const dst = ss.getSheetByName(PCLOUD_SHEET_NAME); if (!src || !dst) return; const authToken = getPcloudAuthToken_(); const srcLast = src.getLastRow(); if (srcLast < 2) return; const dstLast = dst.getLastRow(); const existing = new Set(); if (dstLast >= 2) { // A列(wpidex)を確認 dst.getRange(2, 1, dstLast - 1, 1).getValues().forEach(r => { if(r[0]) existing.add(String(r[0]).trim()); }); } const srcVals = src.getRange(2, 1, srcLast - 1, 4).getValues(); const toAppend = []; for (const r of srcVals) { const wpidex = String(r[1] || '').trim(); const wpPathUrl = String(r[3] || '').trim(); if (!wpidex || existing.has(wpidex)) continue; const filepath = extractFilepathFromWpUrl_(wpPathUrl); if (!filepath) continue; // pCloud生存確認 if (!existsOnPcloudByLocalFilepath_(filepath, authToken)) continue; const filename = filepath.split('/').pop(); // ★追加時の配列構成 (A〜H) // A:wpidex, B:empty, C:filename, D:filepath, E:empty, F:TRUE, G:empty, H:empty toAppend.push([wpidex, '', filename, filepath, '', true, '', '']); existing.add(wpidex); } if (toAppend.length > 0) { dst.getRange(dstLast + 1, 1, toAppend.length, 8).setValues(toAppend); Logger.log('✅ 新規追加: ' + toAppend.length + ' 件'); } } function existsOnPcloudByLocalFilepath_(filepath, authToken) { try { fetchFileidFromLocalFilepath_(filepath, authToken); return true; } catch (e) { return false; } } function extractFilepathFromWpUrl_(url) { if (!url) return ''; const s = String(url).trim(); const i = s.indexOf('/wp-content/'); if (i < 0) return ''; let tail = s.substring(i + '/wp-content/'.length).replace(/^\/+/, ''); if (!(tail.startsWith('mmedia/') || tail.startsWith('pmedia/'))) return ''; try { tail = decodeURI(tail); } catch (e) {} return '/' + tail; } ```