# [メタ情報] # 識別子: pCloud用再生URLおよび JSON_生成_exe # システム名: pCloud用再生URLおよび JSON_生成 # 技術種別: Misc # 機能名: Misc # 使用言語: GAS # 状態: 実行用 # [/メタ情報] 要約:pCloud の Public フォルダ配下にある mmedia/pmedia ファイルを、スプレッドシート「pcloudid」で管理しつつ、filedn 形式のダイレクトURL一覧 JSON(pcloud_library.json)を自動生成する GAS スクリプト。シートの needs_update が TRUE の行だけ処理し、filepath から /stat?path で fileid を補完、または既存 fileid から /stat?fileid でメタ情報を取得してフルパスを再構築する。Public Folder 部分を除いたパスを encodeURI し、filedn ベースURLと結合して direct_url を作成、シートへ書き戻したうえで、有効な行のみを抽出して JSON を生成し、固定IDの Drive ファイルに上書き保存する。Unicode 正規化やフォルダパスキャッシュ、認証トークン取得用の補助関数も含む。 updatePcloudLibrary.gs /** * ============================================ * pCloud 用ライブラリ更新スクリプト(filedn 版) * * - シート: pcloudid * A: wpidex * B: fileid * C: filename * D: filepath(mmedia/pmedia 以下)★新規追加 * E: direct_url * F: needs_update (TRUE/FALSE) * G: last_update (日時) * * - 処理の流れ: * 1) needs_update = TRUE の行だけ: * - fileid が空なら、filepath から fileid を取得(stat?path) * - fileid があれば stat?fileid → パス解決 * 2) パスから「Public Folder/」部分を取り除き、残りを encodeURI でエンコード * 3) filedn ベースURLと結合して direct_url を生成 * 4) pcloud_library.json を Google Drive に書き出し(固定IDに上書き) * 5) 最後に G列(last_update) の降順にソートしてシートを書き戻す * ============================================ */ /** pcloudid シート名 */ const PCLOUD_SHEET_NAME = 'pcloudid'; /** Drive に書き出す JSON ファイル名(論理名) */ const PCLOUD_JSON_FILENAME = 'pcloud_library.json'; /** Drive 上の pcloud_library.json のファイルID(Google Drive 側) */ const PCLOUD_JSON_FILE_ID = '<あなたのID>'; /** pCloud EU API ベース URL */ const PCLOUD_API_BASE = 'https://eapi.pcloud.com'; /** * filedn ベース URL * - あなたのアカウント専用(全ファイル共通) * - 必ず末尾に / が付いている形にしておく */ const PCLOUD_FILEDN_BASE = 'https://filedn.eu/<あなたのpCloudトークン>/'; /** * pCloud 上の「Public フォルダ」の論理名 * - API の stat で folder の name として返ってくる値 * - 通常は "Public Folder" */ const PCLOUD_PUBLIC_ROOT_NAME = 'Public Folder'; /** * 文字列を Unicode NFD に正規化するヘルパ * (環境によって normalize が無い場合はそのまま返す) */ function normalizeToNFD_(s) { if (!s) return s; try { if (typeof s.normalize === 'function') { return s.normalize('NFD'); } } catch (e) { // 何もしない(素の s を返す) } return s; } /** * セットアップ確認用テスト関数 * - pcloudid シートが見えるか * - PCLOUD_EMAIL / PCLOUD_PASSWORD が設定されているか * - 実際に userinfo を叩いて auth が取れるか */ function testPcloudSetup() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(PCLOUD_SHEET_NAME); if (!sheet) { Logger.log('⚠ pcloudid シートが見つかりません'); } else { Logger.log('✅ pcloudid シートが見つかりました。最終行: ' + sheet.getLastRow()); } const props = PropertiesService.getScriptProperties(); const email = props.getProperty('PCLOUD_EMAIL'); const password = props.getProperty('PCLOUD_PASSWORD'); if (!email || !password) { Logger.log('⚠ PCLOUD_EMAIL / PCLOUD_PASSWORD が設定されていません'); return; } else { Logger.log('✅ PCLOUD_EMAIL / PASSWORD が設定されています(中身はログに出しません)'); } const auth = getPcloudAuthToken_(); if (auth) { Logger.log( '✅ userinfo から auth を取得しました(先頭10文字): ' + auth.substring(0, 10) + '...' ); } } /** * ============================================ * エントリーポイント * - 毎分トリガーを付ける想定 * - F1/mediaLibrary.gs からのチェーンでもOK * ============================================ */ function updatePcloudLibrary() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(PCLOUD_SHEET_NAME); if (!sheet) { Logger.log('⚠ pcloudid シートが見つかりません'); return; } let authToken; try { authToken = getPcloudAuthToken_(); } catch (e) { Logger.log('❌ getPcloudAuthToken_ エラー: ' + e); return; } if (!authToken) { Logger.log('⚠ pCloud auth トークンが取得できませんでした'); return; } const lastRow = sheet.getLastRow(); if (lastRow < 2) { Logger.log('pcloudid にデータ行がありません'); writePcloudLibraryJson_([]); // 空 JSON を書き出し return; } // A:G をまとめて取得(7列) const range = sheet.getRange(2, 1, lastRow - 1, 7); const values = range.getValues(); const now = new Date(); let sheetChanged = false; // フォルダ ID → パス のキャッシュ const folderPathCache = {}; values.forEach((row, idx) => { const wpidex = String(row[0]).trim(); // A: wpidex let fileid = String(row[1]).trim(); // B: fileid let filename = String(row[2]).trim(); // C: filename const filepath = String(row[3]).trim(); // D: filepath(mmedia/pmedia 以下) let direct = String(row[4]).trim(); // E: direct_url let needUpd = row[5]; // F: needs_update let lastUpd = row[6]; // G: last_update if (!wpidex) return; // 空行はスキップ // 1) fileid が空で filepath がある場合、filepath から fileid を補完 if (!fileid && filepath) { try { const info = fetchFileidFromLocalFilepath_(filepath, authToken); fileid = String(info.fileid); row[1] = fileid; // B: fileid を書き込む sheetChanged = true; Logger.log( '✅ fileid補完: wpidex=' + wpidex + ', filepath=' + filepath + ', pcloudPath=' + info.pcloudPath + ', fileid=' + fileid ); } catch (e) { Logger.log( '❌ filepathからfileid取得に失敗: wpidex=' + wpidex + ', filepath=' + filepath + ', error=' + e ); // fileid が取れなかったので、この行は今回はスキップ return; } } // ここで fileid が無ければ何もできない if (!fileid) { Logger.log( '⚠ wpidex=' + wpidex + ' は needs_update=TRUE ですが fileid が空なのでスキップ' ); return; } const needUpdateBool = needUpd === true || (typeof needUpd === 'string' && needUpd.toUpperCase() === 'TRUE') || needUpd === 1; if (!needUpdateBool) { // 更新不要。JSON にはこのまま使うので何もしない return; } // 2) fileid がある行について、pCloud API で direct_url を更新 try { // fileid から pCloud のファイルメタ情報取得(親フォルダ ID など) const meta = fetchPcloudFileMetaByFileid_(fileid, authToken); // 親フォルダ ID から完全パス "/Public Folder/.../ファイル名" を生成 const pcloudPath = buildPcloudPathFromMeta_( meta, authToken, folderPathCache ); // pCloud パス → filedn ダイレクト URL へ変換 const directUrl = buildFilednUrlFromPcloudPath_(pcloudPath); // filename が空なら、meta.name を採用 if (!filename && meta.name) { filename = meta.name; } // 値を更新 direct = directUrl; lastUpd = now; needUpd = false; row[2] = filename; // C: filename // row[3] は filepath(今回は触らない) row[4] = direct; // E: direct_url row[5] = false; // F: needs_update row[6] = lastUpd; // G: last_update sheetChanged = true; Logger.log( '✅ 更新: wpidex=' + wpidex + ', fileid=' + fileid + ', pcloudPath=' + pcloudPath + ', filepath(local)=' + filepath ); } catch (e) { Logger.log( '❌ pCloud API エラー: wpidex=' + wpidex + ', fileid=' + fileid + ', error=' + e ); // エラー時は needs_update を残しておき(TRUEのまま)次回再チャレンジ } }); // 3) last_update(G列) で降順ソート(新しいものが上) sortByLastUpdateDesc_(values); // 4) ソート済みデータをシートに反映(毎回書き戻し) range.setValues(values); // 5) 全レコードから pcloud_library.json を書き出し writePcloudLibraryJson_(values); Logger.log('ℹ updatePcloudLibrary 完了。sheetChanged=' + sheetChanged); } /* =========================== * pCloud API 関連ユーティリティ * =========================== */ /** * pCloud にユーザー名+パスワードでログインして auth を取得 */ function getPcloudAuthToken_() { const props = PropertiesService.getScriptProperties(); const email = props.getProperty('PCLOUD_EMAIL'); const password = props.getProperty('PCLOUD_PASSWORD'); if (!email || !password) { throw new Error( 'PCLOUD_EMAIL / PCLOUD_PASSWORD が未設定です(スクリプトプロパティ)' ); } const url = PCLOUD_API_BASE + '/userinfo'; const payload = { getauth: 1, username: email, password: password }; const res = UrlFetchApp.fetch(url, { method: 'post', payload: payload, muteHttpExceptions: true }); const text = res.getContentText(); Logger.log('userinfo raw response: ' + text); const json = JSON.parse(text); if (json.result !== 0) { throw new Error( 'pCloud userinfo error: result=' + json.result + ', error=' + (json.error || '') ); } if (!json.auth) { throw new Error('pCloud userinfo から auth が取得できませんでした'); } return json.auth; } /** * fileid から pCloud API /stat を叩き、ファイルメタ情報を取得 */ function fetchPcloudFileMetaByFileid_(fileid, authToken) { const url = PCLOUD_API_BASE + '/stat?fileid=' + encodeURIComponent(fileid) + '&auth=' + encodeURIComponent(authToken); Logger.log('➡ stat(file) URL: ' + url); const res = UrlFetchApp.fetch(url, { method: 'get', muteHttpExceptions: true }); const text = res.getContentText(); Logger.log('⬅ stat(file) raw response: ' + text); const json = JSON.parse(text); if (json.result !== 0) { throw new Error( 'pCloud stat(file) error: result=' + json.result + ', error=' + (json.error || '') ); } if (!json.metadata) { throw new Error('pCloud stat(file) の metadata が空です'); } return json.metadata; } /** * フォルダ ID から、フルパス "/Public Folder/test" を再帰的に構築 * - folderId = 0 の場合は "/"(ルート) * - キャッシュを使って API 呼び出し回数を削減 */ function resolvePcloudFolderPath_(folderId, authToken, cache) { if (!cache) cache = {}; if (folderId === 0 || folderId === '0' || folderId == null) { return '/'; } const key = String(folderId); if (cache[key]) { return cache[key]; } const url = PCLOUD_API_BASE + '/stat?folderid=' + encodeURIComponent(folderId) + '&auth=' + encodeURIComponent(authToken); Logger.log('➡ stat(folder) URL: ' + url); const res = UrlFetchApp.fetch(url, { method: 'get', muteHttpExceptions: true }); const text = res.getContentText(); Logger.log('⬅ stat(folder) raw response: ' + text); const json = JSON.parse(text); if (json.result !== 0) { throw new Error( 'pCloud stat(folder) error: result=' + json.result + ', error=' + (json.error || '') ); } const meta = json.metadata; if (!meta) { throw new Error('pCloud stat(folder) の metadata が空です'); } const name = meta.name || ''; const parentId = meta.parentfolderid; const parentPath = resolvePcloudFolderPath_(parentId, authToken, cache); let fullPath; if (!name) { fullPath = parentPath || '/'; } else if (parentPath === '/' || parentPath === '') { fullPath = '/' + name; } else if (parentPath.endsWith('/')) { fullPath = parentPath + name; } else { fullPath = parentPath + '/' + name; } cache[key] = fullPath; return fullPath; } /** * ファイル metadata から、完全 pCloud パスを組み立てる * - 例: folderPath="/Public Folder/test", name="これはテストです.png" * → "/Public Folder/test/これはテストです.png" */ function buildPcloudPathFromMeta_(meta, authToken, folderPathCache) { const name = meta.name || ''; const parentFolderId = meta.parentfolderid; const folderPath = resolvePcloudFolderPath_( parentFolderId, authToken, folderPathCache ); if (!name) { return folderPath; } if (!folderPath || folderPath === '/') { return '/' + name; } if (folderPath.endsWith('/')) { return folderPath + name; } else { return folderPath + '/' + name; } } /** * ローカルの filepath("/mmedia/..." or "/pmedia/...")から * pCloud 上のフルパス "/Public Folder/..." を構築し、NFD 化する */ function buildPcloudPathFromLocalFilepath_(filepath) { if (!filepath) { throw new Error('filepath が空です'); } let rel = filepath.trim(); // 例: "/mmedia/xxx" または "mmedia/xxx" if (!rel.startsWith('/')) { rel = '/' + rel; } const rawPath = '/' + PCLOUD_PUBLIC_ROOT_NAME + rel; // "/Public Folder/mmedia/..." const nfdPath = normalizeToNFD_(rawPath); Logger.log( 'buildPcloudPathFromLocalFilepath_: filepath="' + filepath + '" -> rawPath="' + rawPath + '" -> nfdPath="' + nfdPath + '"' ); return nfdPath; } /** * ローカル filepath から pCloud API /stat?path を叩いて fileid を取得 */ function fetchFileidFromLocalFilepath_(filepath, authToken) { const pcloudPath = buildPcloudPathFromLocalFilepath_(filepath); const url = PCLOUD_API_BASE + '/stat?path=' + encodeURIComponent(pcloudPath) + '&auth=' + encodeURIComponent(authToken); Logger.log('➡ stat(path) URL: ' + url); const res = UrlFetchApp.fetch(url, { method: 'get', muteHttpExceptions: true }); const text = res.getContentText(); Logger.log('⬅ stat(path) raw response: ' + text); const json = JSON.parse(text); if (json.result !== 0) { throw new Error( 'pCloud stat(path) error: result=' + json.result + ', error=' + (json.error || '') ); } if (!json.metadata || json.metadata.isfolder) { throw new Error('pCloud stat(path) の metadata が不正です(フォルダか空)'); } const fileid = json.metadata.fileid; if (!fileid) { throw new Error('pCloud stat(path) の metadata に fileid がありません'); } return { fileid: fileid, pcloudPath: pcloudPath }; } /** * pCloud のフルパスから、filedn ダイレクト URL を構築する * * 例: * pcloudPath = "/Public Folder/test/これはテストです.png" */ function buildFilednUrlFromPcloudPath_(pcloudPath) { if (!pcloudPath) { throw new Error('pcloudPath が空です'); } let relPath = pcloudPath; // 先頭の "/" は一旦削る if (relPath.startsWith('/')) { relPath = relPath.substring(1); // "Public Folder/..." } // "Public Folder/" をルートとみなして削る const prefix = PCLOUD_PUBLIC_ROOT_NAME + '/'; // "Public Folder/" if (relPath === PCLOUD_PUBLIC_ROOT_NAME) { relPath = ''; } else if (relPath.startsWith(prefix)) { relPath = relPath.substring(prefix.length); // "mmedia/..." など } // 念のため先頭の "/" を除去 relPath = relPath.replace(/^\/+/, ''); // encodeURI でパス部分だけエンコード("/" は残る) const encodedPath = relPath ? encodeURI(relPath) : ''; // ベース URL 側の末尾スラッシュ補正 let base = PCLOUD_FILEDN_BASE; if (!base.endsWith('/')) { base += '/'; } if (!encodedPath) { return base; // ルート直下 } // 余分な "//" を避けるため、encodedPath 側の先頭 "/" は既に削ってある前提 return base + encodedPath; } /* =========================== * JSON 書き出し関連 * =========================== */ /** * last_update(G列) をソート用にミリ秒へ変換 */ function toMillis_(v) { if (v instanceof Date) return v.getTime(); if (v == null || v === '') return 0; const d = new Date(v); return isNaN(d) ? 0 : d.getTime(); } /** * pcloudid の values(2行目以降)を * G列(last_update) の新しい順にソートする */ function sortByLastUpdateDesc_(values) { values.sort(function(a, b) { // G列はインデックス 6(0始まり) return toMillis_(b[6]) - toMillis_(a[6]); }); } /** * pcloudid シート全体の values から pcloud_library.json を生成して * Google Drive に書き出す(固定IDに上書き) * * @param {Array[]} values - pcloudid の A:G(2行目以降)の配列 */ function writePcloudLibraryJson_(values) { const dataArr = []; values.forEach(row => { const wpidex = String(row[0]).trim(); // A: wpidex const fileid = String(row[1]).trim(); // B: fileid const filename = String(row[2]).trim(); // C: filename // const filepath = String(row[3]).trim(); // D: filepath(JSONには含めない) const direct = String(row[4]).trim(); // E: direct_url const lastUpd = row[6]; // G: last_update if (!wpidex || !fileid || !direct) { // 必須情報が揃っていないものは JSON に含めない return; } dataArr.push({ wpidex: wpidex, fileid: fileid, filename: filename || null, direct_url: direct, last_update: lastUpd ? Utilities.formatDate( new Date(lastUpd), 'Asia/Tokyo', "yyyy-MM-dd'T'HH:mm:ssXXX" ) : null }); }); const jsonText = JSON.stringify(dataArr, null, 2); Logger.log('pcloud_library.json:'); Logger.log(jsonText); // Drive 上の既存ファイル(固定ID)に強制上書き try { const file = DriveApp.getFileById(PCLOUD_JSON_FILE_ID); file.setContent(jsonText); Logger.log( '✅ Drive の既存ファイル(ID指定)に上書きしました: ' + PCLOUD_JSON_FILE_ID ); } catch (e) { // なければ新規作成(念のため) Logger.log( '⚠ 指定IDのファイル取得に失敗したので新規作成します: ' + e ); DriveApp.createFile( PCLOUD_JSON_FILENAME, jsonText, MimeType.PLAIN_TEXT ); } }