gemini-reverse 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/types/image.js +116 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemini-reverse",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Unofficial Node.js client for gemini.google.com — inspired by Gemini-API (Python). Supports streaming, chat sessions, gems, file uploads, and TypeScript.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/types/image.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const axios = require('axios');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const crypto = require('crypto');
6
7
 
7
8
  class Image {
8
9
  constructor({ url, title = '[Image]', alt = '', proxy = null } = {}) {
@@ -12,43 +13,76 @@ class Image {
12
13
  this.proxy = proxy;
13
14
  }
14
15
 
16
+ _getUrlForHash() {
17
+ return this.url;
18
+ }
19
+
15
20
  toString() {
16
21
  const short = this.url.length <= 20 ? this.url : this.url.slice(0, 8) + '...' + this.url.slice(-12);
17
22
  return `Image(title='${this.title}', alt='${this.alt}', url='${short}')`;
18
23
  }
19
24
 
20
- async save({ path: savePath = 'temp', filename = null, cookies = null, verbose = false, skipInvalidFilename = false } = {}) {
21
- let fname = filename || this.url.split('/').pop().split('?')[0];
22
- const match = fname.match(/^(.*\.\w+)/);
23
- if (match) {
24
- fname = match[1];
25
- } else {
26
- if (verbose) console.warn(`Invalid filename: ${fname}`);
27
- if (skipInvalidFilename) return null;
25
+ async save({ path: savePath = 'temp', filename = null, verbose = false } = {}) {
26
+ if (!filename || !path.extname(filename)) {
27
+ const timestamp = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
28
+ const urlHash = crypto.createHash('sha256').update(this._getUrlForHash()).digest('hex').slice(0, 10);
29
+ const baseName = filename ? path.parse(filename).name : 'image';
30
+ filename = `${timestamp}_${urlHash}_${baseName}`;
28
31
  }
29
32
 
30
- const proxyConfig = this.proxy ? (() => { try { const u = new URL(this.proxy); return { protocol: u.protocol.replace(':', ''), host: u.hostname, port: parseInt(u.port) }; } catch { return undefined; } })() : undefined;
33
+ fs.mkdirSync(savePath, { recursive: true });
34
+ return await this._performSave(savePath, filename, verbose);
35
+ }
36
+
37
+ async _performSave(savePath, filename, verbose) {
38
+ const imgUrl = this.url;
39
+ const proxyConfig = this._parseProxy(this.proxy);
40
+ const clientRef = this.client_ref || null;
41
+ const cookies = clientRef ? clientRef.cookies : null;
31
42
 
32
- const res = await axios.get(this.url, {
43
+ const res = await axios.get(imgUrl, {
33
44
  responseType: 'arraybuffer',
34
- headers: cookies ? { 'Cookie': Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ') } : {},
45
+ headers: {
46
+ 'Origin': 'https://gemini.google.com',
47
+ 'Referer': 'https://gemini.google.com/',
48
+ ...(cookies ? { 'Cookie': Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ') } : {}),
49
+ },
35
50
  maxRedirects: 5,
36
51
  ...(proxyConfig ? { proxy: proxyConfig } : {}),
37
52
  });
38
53
 
39
- if (res.status !== 200) throw new Error(`Error downloading image: ${res.status}`);
54
+ if (verbose) console.debug(`HTTP Request: GET ${imgUrl} [${res.status}]`);
40
55
 
41
- const contentType = res.headers['content-type'] || '';
42
- if (contentType && !contentType.includes('image')) {
43
- console.warn(`Content type of ${fname} is not image, but ${contentType}.`);
56
+ if (res.status !== 200) {
57
+ throw new Error(`Error downloading image: ${res.status}`);
44
58
  }
45
59
 
46
- fs.mkdirSync(savePath, { recursive: true });
47
- const dest = path.join(savePath, fname);
60
+ const contentType = (res.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
61
+ let ext = path.extname(filename);
62
+ if (!ext) {
63
+ if (contentType.includes('jpeg') || contentType.includes('jpg')) ext = '.jpg';
64
+ else if (contentType.includes('png')) ext = '.png';
65
+ else if (contentType.includes('webp')) ext = '.webp';
66
+ else if (contentType.includes('gif')) ext = '.gif';
67
+ else ext = '.png';
68
+ filename = filename + ext;
69
+ }
70
+
71
+ const dest = path.join(savePath, filename);
48
72
  fs.writeFileSync(dest, Buffer.from(res.data));
49
- if (verbose) console.log(`Image saved as ${path.resolve(dest)}`);
73
+ if (verbose) console.info(`Image saved as ${path.resolve(dest)}`);
50
74
  return path.resolve(dest);
51
75
  }
76
+
77
+ _parseProxy(proxyStr) {
78
+ if (!proxyStr) return undefined;
79
+ try {
80
+ const u = new URL(proxyStr);
81
+ return { protocol: u.protocol.replace(':', ''), host: u.hostname, port: parseInt(u.port) };
82
+ } catch {
83
+ return undefined;
84
+ }
85
+ }
52
86
  }
53
87
 
54
88
  class WebImage extends Image {
@@ -56,19 +90,72 @@ class WebImage extends Image {
56
90
  }
57
91
 
58
92
  class GeneratedImage extends Image {
59
- constructor({ cookies, ...opts } = {}) {
60
- super(opts);
61
- if (!cookies || Object.keys(cookies).length === 0)
62
- throw new Error('GeneratedImage requires cookies from GeminiClient.');
63
- this.cookies = cookies;
93
+ constructor({ url, title = '[Image]', alt = '', proxy = null, client_ref = null, cid = '', rid = '', rcid = '', image_id = '' } = {}) {
94
+ super({ url, title, alt, proxy });
95
+ this.client_ref = client_ref;
96
+ this.cid = cid;
97
+ this.rid = rid;
98
+ this.rcid = rcid;
99
+ this.image_id = image_id;
64
100
  }
65
101
 
66
- async save({ path: savePath = 'temp', filename = null, cookies = null, verbose = false, skipInvalidFilename = false, fullSize = true } = {}) {
67
- if (fullSize) this.url += '=s2048';
68
- const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
69
- const fname = filename || `${ts}_${this.url.slice(-10)}.png`;
70
- return super.save({ path: savePath, filename: fname, cookies: cookies || this.cookies, verbose, skipInvalidFilename });
102
+ async _performSave(savePath, filename, verbose, fullSize = true) {
103
+ if (fullSize) {
104
+ if (this.client_ref && this.cid && this.rid && this.rcid && this.image_id) {
105
+ try {
106
+ const originalUrl = await this.client_ref._getFullSizeImage(
107
+ this.cid, this.rid, this.rcid, this.image_id,
108
+ );
109
+ if (originalUrl) {
110
+ const proxyConfig = this._parseProxy(this.proxy);
111
+ const cookies = this.client_ref ? this.client_ref.cookies : null;
112
+ const headers = {
113
+ 'Origin': 'https://gemini.google.com',
114
+ 'Referer': 'https://gemini.google.com/',
115
+ ...(cookies ? { 'Cookie': Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ') } : {}),
116
+ };
117
+
118
+ const r1 = await axios.get(`${originalUrl}=d-I?alr=yes`, {
119
+ headers, maxRedirects: 5, ...(proxyConfig ? { proxy: proxyConfig } : {}),
120
+ });
121
+ const r2 = await axios.get(r1.data.trim(), {
122
+ headers, maxRedirects: 5, ...(proxyConfig ? { proxy: proxyConfig } : {}),
123
+ });
124
+ this.url = r2.data.trim();
125
+ return await super._performSave(savePath, filename, verbose);
126
+ }
127
+ } catch (e) {
128
+ if (verbose) console.debug(`Failed to fetch full size image via RPC: ${e.message}, falling back.`);
129
+ }
130
+ }
131
+
132
+ if (this.url.includes('=s1024-rj')) {
133
+ this.url = this.url.replace('=s1024-rj', '=s2048-rj');
134
+ } else if (!this.url.includes('=s2048-rj')) {
135
+ this.url += '=s2048-rj';
136
+ }
137
+ } else {
138
+ if (this.url.includes('=s2048-rj')) {
139
+ this.url = this.url.replace('=s2048-rj', '=s1024-rj');
140
+ } else if (!this.url.includes('=s1024-rj')) {
141
+ this.url += '=s1024-rj';
142
+ }
143
+ }
144
+
145
+ return await super._performSave(savePath, filename, verbose);
146
+ }
147
+
148
+ async save({ path: savePath = 'temp', filename = null, verbose = false, fullSize = true } = {}) {
149
+ if (!filename || !path.extname(filename)) {
150
+ const timestamp = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
151
+ const urlHash = crypto.createHash('sha256').update(this._getUrlForHash()).digest('hex').slice(0, 10);
152
+ const baseName = filename ? path.parse(filename).name : 'image';
153
+ filename = `${timestamp}_${urlHash}_${baseName}`;
154
+ }
155
+
156
+ fs.mkdirSync(savePath, { recursive: true });
157
+ return await this._performSave(savePath, filename, verbose, fullSize);
71
158
  }
72
159
  }
73
160
 
74
- module.exports = { Image, WebImage, GeneratedImage };
161
+ module.exports = { Image, WebImage, GeneratedImage };