UNPKG

gphotos-scraper

Version:

A tool to extract public url and metadata from shared album

172 lines 7.49 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getAlbum = exports.getData = void 0; const exceptions_1 = require("../exceptions"); const axiosClient_1 = require("./axiosClient"); const RPCID = "snAcKc"; const BASE_URL = "https://photos.google.com/_/PhotosUi/data/batchexecute"; // Pre-compiled Regex for better performance const RE_CANONICAL = /<link\s+rel="canonical"\s+href="([^"]+)"/; const RE_KEY = /key=([^"&]+)/; const getData = (id, key, token) => __awaiter(void 0, void 0, void 0, function* () { var _a; const queryData = [[RPCID, JSON.stringify([id, token, null, key])]]; const params = new URLSearchParams(); params.append("f.req", JSON.stringify([queryData])); let data; try { const res = yield axiosClient_1.apiClient.post(BASE_URL, params, { headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", }, }); data = res.data.replace(/^\)\]\}'/, ""); // Faster regex replacement // DEBUG: Log raw RPC response const fs = require('fs'); fs.writeFileSync('tmp/debug_rpc_album.txt', data); } catch (error) { console.error((error === null || error === void 0 ? void 0 : error.message) || error); throw error; } const list = JSON.parse(data); const reponseData = (_a = list.find((item) => (item === null || item === void 0 ? void 0 : item[1]) === RPCID)) === null || _a === void 0 ? void 0 : _a[2]; if (!reponseData) return {}; const parsedResponse = JSON.parse(reponseData); const albumItems = (parsedResponse === null || parsedResponse === void 0 ? void 0 : parsedResponse[1]) || []; const next = parsedResponse === null || parsedResponse === void 0 ? void 0 : parsedResponse[2]; const albumDetails = (parsedResponse === null || parsedResponse === void 0 ? void 0 : parsedResponse[3]) || []; const [albumId, albumTitle, albumDates, downloadUrl, albumCover, albumOwner] = albumDetails; const sharedUrl = albumDetails === null || albumDetails === void 0 ? void 0 : albumDetails[32]; const createdAt = albumDates === null || albumDates === void 0 ? void 0 : albumDates[4]; const updatedAt = albumDates === null || albumDates === void 0 ? void 0 : albumDates[8]; const coverUrl = albumCover === null || albumCover === void 0 ? void 0 : albumCover[0]; const coverWidth = albumCover === null || albumCover === void 0 ? void 0 : albumCover[1]; const coverHeight = albumCover === null || albumCover === void 0 ? void 0 : albumCover[2]; const ownerId = albumOwner === null || albumOwner === void 0 ? void 0 : albumOwner[1]; const ownerDetails = (albumOwner === null || albumOwner === void 0 ? void 0 : albumOwner[11]) || []; const [ownerFullname, , ownerGenre, ownerName] = ownerDetails; const ownerPhoto = albumOwner === null || albumOwner === void 0 ? void 0 : albumOwner[3]; const items = albumItems.map((item) => { const [id, itemData] = item; const [url] = itemData; return { id, url }; }); return { id: albumId, key, token: next, title: albumTitle, url: sharedUrl, downloadUrl, createdAt, updatedAt, cover: { url: coverUrl, width: coverWidth, height: coverHeight, }, owner: { id: ownerId, fullname: ownerFullname, name: ownerName, genre: ownerGenre, photo: ownerPhoto, }, items, }; }); exports.getData = getData; const getAlbum = (url) => __awaiter(void 0, void 0, void 0, function* () { var _a; let data = null; try { const res = yield axiosClient_1.apiClient.get(url); data = res.data; } catch (error) { if (((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === 404 || (error === null || error === void 0 ? void 0 : error.code) === "ERR_BAD_REQUEST") { throw new exceptions_1.AlbumNotFoundException(); } throw error; } // Extract Metadata using Regex (No Cheerio) const canonicalMatch = data.match(RE_CANONICAL); const canonicalUrl = canonicalMatch ? canonicalMatch[1] : ""; if (!canonicalUrl) throw new exceptions_1.AlbumNotFoundException(); const albumUrlObj = new URL(canonicalUrl); const pathParts = albumUrlObj.pathname.split("/"); const id = pathParts[pathParts.length - 1] || pathParts[pathParts.length - 2]; // Handle trailing slash // Key might be in the original URL or need to be extracted from response if redirected // But usually it's in the query param of the canonical or initial URL. // We'll trust the canonical for ID, but for key we check the URL params of the Input URL or the Canonical. let key = albumUrlObj.searchParams.get("key"); if (!key) { // Fallback: try to find key in the HTML if not in URL // (The original code assumed it was in the canonical URL params) // If the original logic worked with `albumUrl.searchParams.get('key')`, we stick to it. // But let's check the input `url` too just in case. try { const inputUrlObj = new URL(url); key = inputUrlObj.searchParams.get("key"); } catch (_b) { } } // Last resort regex if key is still missing (sometimes encoded in JS vars) if (!key) { const keyMatch = data.match(RE_KEY); if (keyMatch) key = keyMatch[1]; } if (!id || !key) throw new exceptions_1.AlbumNotFoundException(); let album; let token; let pageCount = 0; const MAX_PAGES = 100; // Safety limit do { // Safety break if (pageCount++ > MAX_PAGES) break; const parts = yield (0, exports.getData)(id, key, token); if (!album) { // set once on first page if (!parts.id) { // If getData failed to parse on first try throw new exceptions_1.AlbumNotFoundException(); } const { title, owner, cover, downloadUrl, createdAt, updatedAt } = parts; album = { key, id, url, title, token, owner, cover, downloadUrl, createdAt, updatedAt, items: [], }; } token = parts.token; if (parts.items) { album.items.push(...parts.items); } } while (token); return album; }); exports.getAlbum = getAlbum; //# sourceMappingURL=getAlbum.js.map