UNPKG

gphotos-scraper

Version:

A tool to extract public url and metadata from shared album

178 lines (150 loc) 4.75 kB
import { AlbumNotFoundException } from "../exceptions"; import type { GoogleAlbum } from "../interfaces"; import { apiClient } from "./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=([^"&]+)/; export const getData = async ( id: string, key: string, token?: string, ): Promise<GoogleAlbum> => { const queryData = [[RPCID, JSON.stringify([id, token, null, key])]]; const params = new URLSearchParams(); params.append("f.req", JSON.stringify([queryData])); let data: string; try { const res = await apiClient.post(BASE_URL, params, { headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", }, }); data = res.data.replace(/^\)\]\}'/, ""); // Faster regex replacement } catch (error: any) { console.error(error?.message || error); throw error; } const list = JSON.parse(data); const reponseData = list.find((item: any[]) => item?.[1] === RPCID)?.[2]; if (!reponseData) return {} as GoogleAlbum; const parsedResponse = JSON.parse(reponseData); const albumItems = parsedResponse?.[1] || []; const next = parsedResponse?.[2]; const albumDetails = parsedResponse?.[3] || []; const [albumId, albumTitle, albumDates, downloadUrl, albumCover, albumOwner] = albumDetails; const sharedUrl = albumDetails?.[32]; const createdAt = albumDates?.[4]; const updatedAt = albumDates?.[8]; const coverUrl = albumCover?.[0]; const coverWidth = albumCover?.[1]; const coverHeight = albumCover?.[2]; const ownerId = albumOwner?.[1]; const ownerDetails = albumOwner?.[11] || []; const [ownerFullname, , ownerGenre, ownerName] = ownerDetails; const ownerPhoto = albumOwner?.[3]; const items = albumItems.map((item: any[]) => { const [id, itemData] = item; const [url] = itemData as any[]; 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, }; }; export const getAlbum = async (url: string): Promise<GoogleAlbum> => { let data = null; try { const res = await apiClient.get(url); data = res.data; } catch (error: any) { if (error?.response?.status === 404 || error?.code === "ERR_BAD_REQUEST") { throw new AlbumNotFoundException(); } throw error; } // Extract Metadata using Regex (No Cheerio) const canonicalMatch = data.match(RE_CANONICAL); const canonicalUrl = canonicalMatch ? canonicalMatch[1] : ""; if (!canonicalUrl) throw new 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 {} } // 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 AlbumNotFoundException(); let album: GoogleAlbum | undefined; let token: string | undefined; let pageCount = 0; const MAX_PAGES = 100; // Safety limit do { // Safety break if (pageCount++ > MAX_PAGES) break; const parts = await getData(id, key, token); if (!album) { // set once on first page if (!parts.id) { // If getData failed to parse on first try throw new 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!; };