TEST

/************************************************************ TLM PHOTOS — GOOGLE PHOTOS ALBUM API BRIDGE Google Apps Script Web App Album: https://photos.app.goo.gl/TQ7pHKyAV7U3JgAF9 Returns JSON / JSONP for Blogger ************************************************************/ const ALBUM_URL = 'https://photos.app.goo.gl/TQ7pHKyAV7U3JgAF9'; const RPC_ID = 'snAcKc'; const BATCH_URL = 'https://photos.google.com/u/0/_/PhotosUi/data/batchexecute'; const MAX_PAGES = 100; /* ========================================================= WEB APP ========================================================= */ function doGet(e) { try { const result = getAlbumPhotos(); const callback = e && e.parameter && e.parameter.callback; const json = JSON.stringify(result); if (callback) { return ContentService .createTextOutput( callback + '(' + json + ');' ) .setMimeType( ContentService.MimeType.JAVASCRIPT ); } return ContentService .createTextOutput(json) .setMimeType( ContentService.MimeType.JSON ); } catch (error) { const result = { success: false, error: String(error), photos: [] }; const json = JSON.stringify(result); const callback = e && e.parameter && e.parameter.callback; if (callback) { return ContentService .createTextOutput( callback + '(' + json + ');' ) .setMimeType( ContentService.MimeType.JAVASCRIPT ); } return ContentService .createTextOutput(json) .setMimeType( ContentService.MimeType.JSON ); } } /* ========================================================= MAIN ========================================================= */ function getAlbumPhotos() { const cache = CacheService.getScriptCache(); const cached = cache.get('TLM_PHOTOS'); if (cached) { return JSON.parse(cached); } const html = fetchAlbum(ALBUM_URL); /* Extract the main Google Photos AF_initDataCallback object. */ const phase1 = extractInitData(html); if (!phase1) { throw new Error( 'Google Photos album data was not found.' ); } const data = parseGoogleData(phase1); if (!data) { throw new Error( 'Unable to parse Google Photos album data.' ); } let photos = parseMediaItems(data); /* Extract pagination information. */ let pageToken = getNextPageToken(data); /* Extract album request keys. */ const request = extractAlbumRequest(html); /* Fetch remaining pages. */ if ( pageToken && request ) { let page = 1; const seen = {}; while ( pageToken && page < MAX_PAGES ) { if (seen[pageToken]) { break; } seen[pageToken] = true; const response = getAlbumPage( request, pageToken ); if (!response) { break; } const pageData = response.data; const more = parseMediaItems({ data: pageData }); if (more && more.length) { photos = photos.concat(more); } pageToken = response.nextPageToken; page++; } } /* Remove duplicate URLs. */ const unique = {}; const finalPhotos = photos.filter(function(photo) { if (!photo.url) { return false; } if (unique[photo.url]) { return false; } unique[photo.url] = true; return true; }); const result = { success: true, album: ALBUM_URL, count: finalPhotos.length, updated: new Date().toISOString(), photos: finalPhotos }; /* Cache for 5 minutes. */ cache.put( 'TLM_PHOTOS', JSON.stringify(result), 300 ); return result; } /* ========================================================= FETCH ALBUM ========================================================= */ function fetchAlbum(url) { const response = UrlFetchApp.fetch( url, { method: 'get', followRedirects: true, muteHttpExceptions: true, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/140 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml' } } ); const code = response.getResponseCode(); if (code < 200 || code >= 400) { throw new Error( 'Google Photos returned HTTP ' + code ); } return response.getContentText(); } /* ========================================================= EXTRACT AF_initDataCallback ========================================================= */ function extractInitData(html) { const regex = /AF_initDataCallback\((\{[\s\S]*?)(\);<\/script>)/g; let match; let best = ''; while ( (match = regex.exec(html)) ) { const candidate = match[1] || ''; if ( candidate.length > best.length ) { best = candidate; } } return best || null; } /* ========================================================= PARSE GOOGLE DATA Google Photos currently emits JSON5-like JavaScript data. Apps Script V8 can evaluate the extracted object. ========================================================= */ function parseGoogleData(text) { try { return Function( '"use strict"; return (' + text + ');' )(); } catch (error) { throw new Error( 'Google Photos data parsing failed: ' + error ); } } /* ========================================================= PARSE MEDIA ITEMS Structure follows the public shared-page metadata used by current extractors. ========================================================= */ function parseMediaItems(input) { if ( !input || !Array.isArray(input.data) ) { return []; } const arr = input.data[1]; if ( !Array.isArray(arr) ) { return []; } const result = []; arr.forEach(function(item) { if ( !Array.isArray(item) || item.length < 6 ) { return; } const uid = item[0]; const detail = item[1]; if ( typeof uid !== 'string' || !Array.isArray(detail) || detail.length < 3 ) { return; } const url = detail[0]; const width = detail[1]; const height = detail[2]; if ( typeof url !== 'string' || typeof width !== 'number' || typeof height !== 'number' ) { return; } result.push({ uid: uid, url: url, width: width, height: height }); }); return result; } /* ========================================================= NEXT PAGE TOKEN ========================================================= */ function getNextPageToken(input) { if ( !input || !Array.isArray(input.data) ) { return null; } const token = input.data[2]; return ( typeof token === 'string' && token.length > 0 ) ? token : null; } /* ========================================================= EXTRACT ALBUM KEY + AUTH KEY ========================================================= */ function extractAlbumRequest(html) { const regex = /snAcKc[^}]*?request:\s*\[\s*"([A-Za-z0-9_-]+)"\s*,\s*null\s*,\s*null\s*,\s*"([A-Za-z0-9_-]+)"/s; const match = regex.exec(html); if (!match) { return null; } return { albumKey: match[1], authKey: match[2] }; } /* ========================================================= GET PAGINATED PAGE ========================================================= */ function getAlbumPage( request, pageToken ) { const innerRequest = JSON.stringify([ request.albumKey, pageToken, null, request.authKey ]); const envelope = JSON.stringify([ [ [ RPC_ID, innerRequest, null, 'generic' ] ] ]); const url = BATCH_URL + '?rpcids=' + RPC_ID + '&source-path=/share/' + encodeURIComponent( request.albumKey ); const response = UrlFetchApp.fetch( url, { method: 'post', muteHttpExceptions: true, headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Origin': 'https://photos.google.com', 'Referer': 'https://photos.google.com/share/' + request.albumKey + '?key=' + request.authKey }, payload: { 'f.req': envelope } } ); const body = response.getContentText(); return parseBatchResponse(body); } /* ========================================================= PARSE BATCH RESPONSE ========================================================= */ function parseBatchResponse(body) { let text = body.trim(); if ( text.indexOf(")]}'") === 0 ) { text = text.substring(4).trim(); } const lines = text.split('\n'); let responseLine = null; for ( let i = 0; i < lines.length; i++ ) { if ( lines[i] .trim() .indexOf('[[') === 0 ) { responseLine = lines[i] .trim(); break; } } if (!responseLine) { return null; } try { const outer = JSON.parse( responseLine ); let payload = null; outer.forEach(function(entry) { if ( Array.isArray(entry) && entry[0] === 'wrb.fr' && entry[1] === RPC_ID && typeof entry[2] === 'string' ) { payload = entry[2]; } }); if (!payload) { return null; } const data = JSON.parse(payload); return { data: data, nextPageToken: ( Array.isArray(data) && typeof data[2] === 'string' ) ? data[2] : null }; } catch (error) { return null; } }

Post a Comment

0 Comments