gphotos-scraper
Version:
A tool to extract public url and metadata from shared album
70 lines (57 loc) • 1.62 kB
text/typescript
import { lookup } from "mime-types";
import type { GooglePhoto } from "../interfaces";
import { apiClient } from "./axiosClient";
const RPCID = "fDcn4b";
const BASE_URL = "https://photos.google.com/_/PhotosUi/data/batchexecute";
export const getPhotos = async (
key: string,
photoIds: string[],
): Promise<GooglePhoto[]> => {
const queryData = photoIds.map((photoId) => [
RPCID,
JSON.stringify([photoId, null, key]),
]);
const params = new URLSearchParams();
params.append("f.req", JSON.stringify([queryData]));
const { data } = await apiClient.post(BASE_URL, params, {
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
});
// Security: Anti-hijacking cleanup
const cleanData = data.replace(/^\)\]\}'/, "");
let list: any[];
try {
list = JSON.parse(cleanData);
} catch {
return [];
}
// Optimized single-pass reduction
return list.reduce((acc: GooglePhoto[], item: any[]) => {
if (item?.[1] !== RPCID) return acc;
try {
const responseText = item?.[2];
if (!responseText) return acc;
const jsonObj = JSON.parse(responseText);
const photoData = jsonObj?.[0];
if (!photoData) return acc;
const [id, description, filename, createdAt, , size, width, height] =
photoData;
const mimeType = lookup(filename as string) || "application/octet-stream";
acc.push({
id,
url: "", // Will be populated in extractAlbum
description,
filename,
createdAt,
size,
width,
height,
mimeType,
});
} catch {
// Silently ignore parsing failures for individual items
}
return acc;
}, []);
};