Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate text and images via the reverse-engineered Gemini Web API with multi-turn conversation support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/gemini-webapi/types/image.ts
1import path from 'node:path';2import { mkdir, writeFile } from 'node:fs/promises';34import { logger } from '../utils/logger.js';5import { cookie_header, fetch_with_timeout } from '../utils/http.js';67export class Image {8constructor(9public url: string,10public title = '[Image]',11public alt = '',12public proxy: string | null = null,13) {}1415toString(): string {16const u = this.url.length <= 20 ? this.url : `${this.url.slice(0, 8)}...${this.url.slice(-12)}`;17return `Image(title='${this.title}', alt='${this.alt}', url='${u}')`;18}1920async save(21p: string = 'temp',22filename: string | null = null,23cookies: Record<string, string> | null = null,24verbose: boolean = false,25skip_invalid_filename: boolean = false,26): Promise<string | null> {27filename = filename ?? this.url.split('/').pop()?.split('?')[0] ?? 'image';28const m = filename.match(/^(.*\.\w+)/);29if (m) filename = m[1]!;30else {31if (verbose) logger.warning(`Invalid filename: ${filename}`);32if (skip_invalid_filename) return null;33}3435const headers: Record<string, string> = {36'User-Agent': 'Mozilla/5.0',37Accept: 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8',38Referer: 'https://gemini.google.com/',39};40if (cookies) headers.Cookie = cookie_header(cookies);4142let url = this.url;43let res: Response | null = null;44for (let i = 0; i < 10; i++) {45res = await fetch_with_timeout(url, {46method: 'GET',47headers,48redirect: 'manual',49timeout_ms: 30_000,50});5152if (res.status >= 300 && res.status < 400) {53const loc = res.headers.get('location');54if (!loc) break;55url = new URL(loc, url).toString();56continue;57}5859break;60}6162if (!res) throw new Error('Image download failed: no response');6364if (!res.ok) {65throw new Error(`Error downloading image: ${res.status} ${res.statusText}`);66}6768const ct = res.headers.get('content-type');69if (ct && !ct.includes('image')) {70logger.warning(`Content type of ${filename} is not image, but ${ct}.`);71}7273const dir = path.resolve(p);74await mkdir(dir, { recursive: true });7576const dest = path.join(dir, filename);77const buf = Buffer.from(await res.arrayBuffer());78await writeFile(dest, buf);7980if (verbose) logger.info(`Image saved as ${dest}`);81return dest;82}83}8485export class WebImage extends Image {}8687export class GeneratedImage extends Image {88constructor(89url: string,90title: string,91alt: string,92proxy: string | null,93public cookies: Record<string, string>,94) {95super(url, title, alt, proxy);96if (!cookies || Object.keys(cookies).length === 0) {97throw new Error('GeneratedImage is designed to be initialized with same cookies as GeminiClient.');98}99}100101async save(102p: string = 'temp',103filename: string | null = null,104cookies: Record<string, string> | null = null,105verbose: boolean = false,106skip_invalid_filename: boolean = false,107full_size: boolean = true,108): Promise<string | null> {109const u = full_size ? `${this.url}=s2048` : this.url;110const f = filename ?? `${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}_${u.slice(-10)}.png`;111const img = new Image(u, this.title, this.alt, this.proxy);112return await img.save(p, f, cookies ?? this.cookies, verbose, skip_invalid_filename);113}114}115