gemini-reverse 1.0.1 → 1.0.3

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.
package/index.js CHANGED
@@ -1,9 +1,22 @@
1
1
  'use strict';
2
2
 
3
3
  const { GeminiClient, ChatSession } = require('./client');
4
- const { Model, GRPC, Endpoint, Headers, ErrorCode, TEMPORARY_CHAT_FLAG_INDEX } = require('./constants');
5
- const { AuthError, APIError, ImageGenerationError, GeminiError, TimeoutError, UsageLimitExceeded, ModelInvalid, TemporarilyBlocked } = require('./exceptions');
6
- const { Candidate, ConversationTurn, Gem, GemJar, RPCData, Image, WebImage, GeneratedImage, ModelOutput } = require('./types');
4
+ const {
5
+ Model, GRPC, Endpoint, Headers, ErrorCode, AccountStatus,
6
+ TEMPORARY_CHAT_FLAG_INDEX, STREAMING_FLAG_INDEX, GEM_FLAG_INDEX,
7
+ CARD_CONTENT_RE, ARTIFACTS_RE, DEFAULT_METADATA, MODEL_HEADER_KEY,
8
+ buildModelHeader,
9
+ } = require('./constants');
10
+ const {
11
+ AuthError, APIError, ImageGenerationError, GeminiError, TimeoutError,
12
+ UsageLimitExceeded, ModelInvalid, TemporarilyBlocked,
13
+ } = require('./exceptions');
14
+ const {
15
+ Candidate, ChatHistory, ChatTurn, ChatInfo, Gem, GemJar, RPCData,
16
+ Image, WebImage, GeneratedImage, ModelOutput,
17
+ DeepResearchPlan, DeepResearchStatus, DeepResearchResult,
18
+ Video, GeneratedVideo, GeneratedMedia, AvailableModel,
19
+ } = require('./types');
7
20
 
8
21
  module.exports = {
9
22
  GeminiClient,
@@ -13,7 +26,15 @@ module.exports = {
13
26
  Endpoint,
14
27
  Headers,
15
28
  ErrorCode,
29
+ AccountStatus,
16
30
  TEMPORARY_CHAT_FLAG_INDEX,
31
+ STREAMING_FLAG_INDEX,
32
+ GEM_FLAG_INDEX,
33
+ CARD_CONTENT_RE,
34
+ ARTIFACTS_RE,
35
+ DEFAULT_METADATA,
36
+ MODEL_HEADER_KEY,
37
+ buildModelHeader,
17
38
  AuthError,
18
39
  APIError,
19
40
  ImageGenerationError,
@@ -23,7 +44,9 @@ module.exports = {
23
44
  ModelInvalid,
24
45
  TemporarilyBlocked,
25
46
  Candidate,
26
- ConversationTurn,
47
+ ChatHistory,
48
+ ChatTurn,
49
+ ChatInfo,
27
50
  Gem,
28
51
  GemJar,
29
52
  RPCData,
@@ -31,4 +54,11 @@ module.exports = {
31
54
  WebImage,
32
55
  GeneratedImage,
33
56
  ModelOutput,
34
- };
57
+ DeepResearchPlan,
58
+ DeepResearchStatus,
59
+ DeepResearchResult,
60
+ Video,
61
+ GeneratedVideo,
62
+ GeneratedMedia,
63
+ AvailableModel,
64
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemini-reverse",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ const { buildModelHeader, MODEL_HEADER_KEY, Model } = require('../constants');
4
+
5
+ class AvailableModel {
6
+ constructor({ model_id, model_name, display_name, description, capacity, capacity_field = 12, is_available = true } = {}) {
7
+ this.model_id = model_id;
8
+ this.model_name = model_name;
9
+ this.display_name = display_name;
10
+ this.description = description;
11
+ this.capacity = capacity;
12
+ this.capacity_field = capacity_field;
13
+ this.is_available = is_available;
14
+ }
15
+
16
+ get model_header() {
17
+ let tail;
18
+ if (this.capacity_field === 13) {
19
+ tail = `null,${this.capacity}`;
20
+ } else {
21
+ tail = String(this.capacity);
22
+ }
23
+ return buildModelHeader(this.model_id, tail);
24
+ }
25
+
26
+ get advanced_only() {
27
+ return !(this.capacity === 1 && this.capacity_field === 12);
28
+ }
29
+
30
+ toString() {
31
+ return this.model_name || this.display_name;
32
+ }
33
+
34
+ repr() {
35
+ return `AvailableModel(model_id=${this.model_id}, model_name=${this.model_name}, description=${this.description})`;
36
+ }
37
+
38
+ static computeCapacity(tierFlags, capabilityFlags) {
39
+ if (tierFlags.includes(21)) return [1, 13];
40
+ if (tierFlags.includes(22)) return [2, 13];
41
+ if (capabilityFlags.includes(115)) return [4, 12];
42
+ if (tierFlags.includes(16) || capabilityFlags.includes(106)) return [3, 12];
43
+ if (tierFlags.includes(8) || (!capabilityFlags.includes(106) && capabilityFlags.includes(19))) return [2, 12];
44
+ return [1, 12];
45
+ }
46
+
47
+ static buildModelIdNameMapping() {
48
+ const result = {};
49
+ const keys = [
50
+ 'BASIC_PRO', 'BASIC_FLASH', 'BASIC_THINKING',
51
+ 'PLUS_PRO', 'PLUS_FLASH', 'PLUS_THINKING',
52
+ 'ADVANCED_PRO', 'ADVANCED_FLASH', 'ADVANCED_THINKING',
53
+ ];
54
+ for (const key of keys) {
55
+ const member = Model[key];
56
+ if (!member) continue;
57
+ const headerValue = member.model_header[MODEL_HEADER_KEY];
58
+ if (!headerValue) continue;
59
+ try {
60
+ const parsed = JSON.parse(headerValue);
61
+ const modelId = parsed && parsed[4];
62
+ if (modelId && !(modelId in result)) {
63
+ const baseSuffix = key.split('_').slice(1).join('_');
64
+ const baseKey = 'BASIC_' + baseSuffix;
65
+ const baseMember = Model[baseKey] || member;
66
+ result[modelId] = baseMember.model_name;
67
+ }
68
+ } catch {
69
+ continue;
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+ }
75
+
76
+ module.exports = { AvailableModel };
@@ -2,26 +2,40 @@
2
2
 
3
3
  const { WebImage, GeneratedImage } = require('./image');
4
4
 
5
+ function decodeHtml(str) {
6
+ if (!str) return str;
7
+ return str
8
+ .replace(/&/g, '&')
9
+ .replace(/&lt;/g, '<')
10
+ .replace(/&gt;/g, '>')
11
+ .replace(/&quot;/g, '"')
12
+ .replace(/&#39;/g, "'")
13
+ .replace(/&apos;/g, "'");
14
+ }
15
+
5
16
  class Candidate {
6
- constructor({ rcid, text, text_delta = null, thoughts = null, thoughts_delta = null, web_images = [], generated_images = [] } = {}) {
17
+ constructor({
18
+ rcid,
19
+ text,
20
+ text_delta = null,
21
+ thoughts = null,
22
+ thoughts_delta = null,
23
+ web_images = [],
24
+ generated_images = [],
25
+ generated_videos = [],
26
+ generated_media = [],
27
+ deep_research_plan = null,
28
+ } = {}) {
7
29
  this.rcid = rcid;
8
- this.text = this._decodeHtml(text);
30
+ this.text = decodeHtml(text);
9
31
  this.text_delta = text_delta;
10
- this.thoughts = thoughts ? this._decodeHtml(thoughts) : null;
32
+ this.thoughts = thoughts ? decodeHtml(thoughts) : null;
11
33
  this.thoughts_delta = thoughts_delta;
12
34
  this.web_images = web_images;
13
35
  this.generated_images = generated_images;
14
- }
15
-
16
- _decodeHtml(str) {
17
- if (!str) return str;
18
- return str
19
- .replace(/&amp;/g, '&')
20
- .replace(/&lt;/g, '<')
21
- .replace(/&gt;/g, '>')
22
- .replace(/&quot;/g, '"')
23
- .replace(/&#39;/g, '\'')
24
- .replace(/&apos;/g, '\'');
36
+ this.generated_videos = generated_videos;
37
+ this.generated_media = generated_media;
38
+ this.deep_research_plan = deep_research_plan;
25
39
  }
26
40
 
27
41
  get images() {
@@ -31,9 +45,9 @@ class Candidate {
31
45
  toString() { return this.text; }
32
46
 
33
47
  repr() {
34
- const preview = this.text.length <= 20 ? this.text : this.text.slice(0, 20) + '...';
35
- return `Candidate(rcid='${this.rcid}', text='${preview}', images=${this.images.length})`;
48
+ const preview = this.text && this.text.length <= 100 ? this.text : (this.text ? this.text.slice(0, 100) + '...' : '');
49
+ return `Candidate(rcid=${this.rcid}, text=${preview}, images=${this.images.length}, videos=${this.generated_videos.length}, media=${this.generated_media.length})`;
36
50
  }
37
51
  }
38
52
 
39
- module.exports = { Candidate };
53
+ module.exports = { Candidate };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ class ChatTurn {
4
+ constructor({ role, text, model_output = null } = {}) {
5
+ this.role = role;
6
+ this.text = text;
7
+ this.model_output = model_output;
8
+ }
9
+
10
+ toString() {
11
+ const preview = this.text && this.text.length > 100 ? this.text.slice(0, 100) + '...' : (this.text || '');
12
+ return `${this.role.toUpperCase()}: ${preview}`;
13
+ }
14
+
15
+ repr() {
16
+ const preview = this.text && this.text.length > 100 ? this.text.slice(0, 100) + '...' : (this.text || '');
17
+ return `ChatTurn(role=${this.role}, text=${preview})`;
18
+ }
19
+ }
20
+
21
+ class ChatHistory {
22
+ constructor({ cid, turns } = {}) {
23
+ this.cid = cid;
24
+ this.turns = turns || [];
25
+ }
26
+
27
+ toString() {
28
+ return `ChatHistory(cid=${this.cid})`;
29
+ }
30
+
31
+ repr() {
32
+ return `ChatHistory(cid=${this.cid}, turns=${JSON.stringify(this.turns)})`;
33
+ }
34
+ }
35
+
36
+ module.exports = { ChatTurn, ChatHistory };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ class ChatInfo {
4
+ constructor({ cid, title, is_pinned = false, timestamp } = {}) {
5
+ this.cid = cid;
6
+ this.title = title;
7
+ this.is_pinned = is_pinned;
8
+ this.timestamp = timestamp;
9
+ }
10
+
11
+ toString() {
12
+ const pin = this.is_pinned ? '[Pinned] ' : '';
13
+ const title = this.title || `Chat(${this.cid})`;
14
+ const dt = new Date(this.timestamp * 1000).toISOString().replace('T', ' ').slice(0, 19);
15
+ return `${pin}${title} (${dt})`;
16
+ }
17
+
18
+ repr() {
19
+ return `ChatInfo(cid=${this.cid}, title=${this.title}, is_pinned=${this.is_pinned}, timestamp=${this.timestamp})`;
20
+ }
21
+ }
22
+
23
+ module.exports = { ChatInfo };
package/types/index.js CHANGED
@@ -1,10 +1,34 @@
1
1
  'use strict';
2
2
 
3
3
  const { Candidate } = require('./candidate');
4
- const { ConversationTurn } = require('./conversation');
4
+ const { ChatHistory, ChatTurn } = require('./chathistory');
5
+ const { ChatInfo } = require('./chatinfo');
5
6
  const { Gem, GemJar } = require('./gem');
6
7
  const { RPCData } = require('./grpc');
7
8
  const { Image, WebImage, GeneratedImage } = require('./image');
8
9
  const { ModelOutput } = require('./modeloutput');
10
+ const { DeepResearchPlan, DeepResearchStatus } = require('./research');
11
+ const { DeepResearchResult } = require('./researchresult');
12
+ const { Video, GeneratedVideo, GeneratedMedia } = require('./video');
13
+ const { AvailableModel } = require('./availablemodel');
9
14
 
10
- module.exports = { Candidate, ConversationTurn, Gem, GemJar, RPCData, Image, WebImage, GeneratedImage, ModelOutput };
15
+ module.exports = {
16
+ Candidate,
17
+ ChatHistory,
18
+ ChatTurn,
19
+ ChatInfo,
20
+ Gem,
21
+ GemJar,
22
+ RPCData,
23
+ Image,
24
+ WebImage,
25
+ GeneratedImage,
26
+ ModelOutput,
27
+ DeepResearchPlan,
28
+ DeepResearchStatus,
29
+ DeepResearchResult,
30
+ Video,
31
+ GeneratedVideo,
32
+ GeneratedMedia,
33
+ AvailableModel,
34
+ };
@@ -1,39 +1,27 @@
1
1
  'use strict';
2
2
 
3
- const { WebImage, GeneratedImage } = require('./image');
4
-
5
- class Candidate {
6
- constructor({ rcid, text, text_delta = null, thoughts = null, thoughts_delta = null, web_images = [], generated_images = [] } = {}) {
7
- this.rcid = rcid;
8
- this.text = this._decodeHtml(text);
9
- this.text_delta = text_delta;
10
- this.thoughts = thoughts ? this._decodeHtml(thoughts) : null;
11
- this.thoughts_delta = thoughts_delta;
12
- this.web_images = web_images;
13
- this.generated_images = generated_images;
14
- }
15
-
16
- _decodeHtml(str) {
17
- if (!str) return str;
18
- return str
19
- .replace(/&amp;/g, '&')
20
- .replace(/&lt;/g, '<')
21
- .replace(/&gt;/g, '>')
22
- .replace(/&quot;/g, '"')
23
- .replace(/&#39;/g, '\'')
24
- .replace(/&apos;/g, '\'');
3
+ class ModelOutput {
4
+ constructor(metadata, candidates, chosen = 0) {
5
+ this.metadata = metadata;
6
+ this.candidates = candidates;
7
+ this.chosen = chosen;
25
8
  }
26
9
 
27
- get images() {
28
- return [...this.web_images, ...this.generated_images];
29
- }
10
+ get text() { return this.candidates[this.chosen].text; }
11
+ get text_delta() { return this.candidates[this.chosen].text_delta || ''; }
12
+ get thoughts() { return this.candidates[this.chosen].thoughts; }
13
+ get thoughts_delta() { return this.candidates[this.chosen].thoughts_delta || ''; }
14
+ get images() { return this.candidates[this.chosen].images; }
15
+ get videos() { return this.candidates[this.chosen].generated_videos || []; }
16
+ get media() { return this.candidates[this.chosen].generated_media || []; }
17
+ get deep_research_plan() { return this.candidates[this.chosen].deep_research_plan || null; }
18
+ get rcid() { return this.candidates[this.chosen].rcid; }
30
19
 
31
20
  toString() { return this.text; }
32
21
 
33
22
  repr() {
34
- const preview = this.text.length <= 20 ? this.text : this.text.slice(0, 20) + '...';
35
- return `Candidate(rcid='${this.rcid}', text='${preview}', images=${this.images.length})`;
23
+ return `ModelOutput(metadata=${JSON.stringify(this.metadata)}, chosen=${this.chosen}, candidates=${this.candidates.length})`;
36
24
  }
37
25
  }
38
26
 
39
- module.exports = { Candidate };
27
+ module.exports = { ModelOutput };
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ class DeepResearchPlan {
4
+ constructor({
5
+ research_id = null,
6
+ title = null,
7
+ query = null,
8
+ steps = [],
9
+ eta_text = null,
10
+ confirm_prompt = null,
11
+ modify_prompt = null,
12
+ confirmation_url = null,
13
+ metadata = [],
14
+ cid = null,
15
+ response_text = null,
16
+ raw_state = null,
17
+ } = {}) {
18
+ this.research_id = research_id;
19
+ this.title = title;
20
+ this.query = query;
21
+ this.steps = steps;
22
+ this.eta_text = eta_text;
23
+ this.confirm_prompt = confirm_prompt;
24
+ this.modify_prompt = modify_prompt;
25
+ this.confirmation_url = confirmation_url;
26
+ this.metadata = metadata;
27
+ this.cid = cid;
28
+ this.response_text = response_text;
29
+ this.raw_state = raw_state;
30
+ }
31
+
32
+ repr() {
33
+ return `DeepResearchPlan(research_id=${this.research_id}, title=${this.title}, eta_text=${this.eta_text}, metadata=${JSON.stringify(this.metadata)})`;
34
+ }
35
+ }
36
+
37
+ class DeepResearchStatus {
38
+ constructor({
39
+ research_id,
40
+ state = 'running',
41
+ title = null,
42
+ query = null,
43
+ cid = null,
44
+ notes = [],
45
+ done = false,
46
+ raw_state = null,
47
+ raw = null,
48
+ } = {}) {
49
+ this.research_id = research_id;
50
+ this.state = state;
51
+ this.title = title;
52
+ this.query = query;
53
+ this.cid = cid;
54
+ this.notes = notes;
55
+ this.done = done;
56
+ this.raw_state = raw_state;
57
+ this.raw = raw;
58
+ }
59
+
60
+ repr() {
61
+ return `DeepResearchStatus(research_id=${this.research_id}, state=${this.state}, title=${this.title}, done=${this.done})`;
62
+ }
63
+ }
64
+
65
+ module.exports = { DeepResearchPlan, DeepResearchStatus };
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ class DeepResearchResult {
4
+ constructor({ plan, start_output = null, final_output = null, statuses = [], done = false } = {}) {
5
+ this.plan = plan;
6
+ this.start_output = start_output;
7
+ this.final_output = final_output;
8
+ this.statuses = statuses;
9
+ this.done = done;
10
+ }
11
+
12
+ get text() {
13
+ return this.final_output ? this.final_output.text : '';
14
+ }
15
+
16
+ repr() {
17
+ return `DeepResearchResult(plan=${this.plan ? this.plan.repr() : null}, done=${this.done}, final_output=${this.final_output})`;
18
+ }
19
+ }
20
+
21
+ module.exports = { DeepResearchResult };
package/types/video.js ADDED
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const axios = require('axios');
6
+ const crypto = require('crypto');
7
+
8
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
9
+
10
+ class Video {
11
+ constructor({ url, title = '[Video]', proxy = null } = {}) {
12
+ this.url = url;
13
+ this.title = title;
14
+ this.proxy = proxy;
15
+ this._defaultFilenameSuffix = 'video';
16
+ }
17
+
18
+ _getUrlForHash() {
19
+ return this.url;
20
+ }
21
+
22
+ repr() {
23
+ return `Video(title=${this.title}, url=${this.url})`;
24
+ }
25
+
26
+ async save({ savePath = 'temp', filename = null, verbose = false } = {}) {
27
+ if (!filename || !path.extname(filename)) {
28
+ const timestamp = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
29
+ const urlHash = crypto.createHash('sha256').update(this._getUrlForHash()).digest('hex').slice(0, 10);
30
+ const baseName = filename ? path.parse(filename).name : this._defaultFilenameSuffix;
31
+ filename = `${timestamp}_${urlHash}_${baseName}`;
32
+ }
33
+
34
+ if (!fs.existsSync(savePath)) fs.mkdirSync(savePath, { recursive: true });
35
+
36
+ return await this._performSave(savePath, filename, verbose);
37
+ }
38
+
39
+ async _performSave(savePath, filename, verbose) {
40
+ const filePath = await this._downloadFile(this.url, savePath, filename, '.mp4', verbose);
41
+ return { video: filePath, video_thumbnail: null };
42
+ }
43
+
44
+ async _downloadFile(url, savePath, filename, defaultExt = '.mp4', verbose = false) {
45
+ const proxyConfig = this.proxy ? this._parseProxy(this.proxy) : undefined;
46
+ const res = await axios.get(url, {
47
+ responseType: 'arraybuffer',
48
+ ...(proxyConfig ? { proxy: proxyConfig } : {}),
49
+ validateStatus: null,
50
+ });
51
+
52
+ if (verbose) console.debug(`HTTP Request: GET ${url} [${res.status}]`);
53
+
54
+ if (res.status === 200) {
55
+ let ext = defaultExt;
56
+ const contentType = (res.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
57
+ if (contentType.includes('mp4')) ext = '.mp4';
58
+ else if (contentType.includes('mp3') || contentType.includes('mpeg')) ext = '.mp3';
59
+ else if (contentType.includes('jpeg')) ext = '.jpg';
60
+ else if (contentType.includes('png')) ext = '.png';
61
+ else if (contentType.includes('webm')) ext = '.webm';
62
+
63
+ if (!path.extname(filename)) filename = filename + ext;
64
+ const dest = path.join(savePath, filename);
65
+ fs.writeFileSync(dest, Buffer.from(res.data));
66
+ if (verbose) console.info(`File saved as ${path.resolve(dest)}`);
67
+ return path.resolve(dest);
68
+ } else if (res.status === 206) {
69
+ return '206';
70
+ } else {
71
+ throw new Error(`Error downloading file: ${res.status}`);
72
+ }
73
+ }
74
+
75
+ _parseProxy(proxyStr) {
76
+ try {
77
+ const u = new URL(proxyStr);
78
+ return { protocol: u.protocol.replace(':', ''), host: u.hostname, port: parseInt(u.port) };
79
+ } catch {
80
+ return undefined;
81
+ }
82
+ }
83
+ }
84
+
85
+ class GeneratedVideo extends Video {
86
+ constructor({ url, title = '[Video]', proxy = null, thumbnail = '', cid = '', rid = '', rcid = '', client_ref = null } = {}) {
87
+ super({ url, title, proxy });
88
+ this.thumbnail = thumbnail;
89
+ this.cid = cid;
90
+ this.rid = rid;
91
+ this.rcid = rcid;
92
+ this.client_ref = client_ref;
93
+ }
94
+
95
+ async _performSave(savePath, filename, verbose) {
96
+ let thumbPath = null;
97
+ if (this.thumbnail) {
98
+ const thumbBase = path.parse(filename).name;
99
+ try {
100
+ thumbPath = await this._downloadFile(this.thumbnail, savePath, thumbBase, '.jpg', verbose);
101
+ } catch (e) {
102
+ if (verbose) console.warn(`Failed to save thumbnail: ${e.message}`);
103
+ }
104
+ }
105
+
106
+ while (true) {
107
+ const videoPath = await this._downloadFile(this.url, savePath, filename, '.mp4', verbose);
108
+ if (videoPath === '206') {
109
+ if (verbose) console.info('Video still generating (206), retrying in 10s...');
110
+ await sleep(10000);
111
+ } else {
112
+ return { video: videoPath, video_thumbnail: thumbPath };
113
+ }
114
+ }
115
+ }
116
+
117
+ repr() {
118
+ return `GeneratedVideo(title=${this.title}, url=${this.url})`;
119
+ }
120
+ }
121
+
122
+ class GeneratedMedia extends GeneratedVideo {
123
+ constructor({ url, title = '[Media]', proxy = null, thumbnail = '', mp3_url = '', mp3_thumbnail = '', cid = '', rid = '', rcid = '', client_ref = null } = {}) {
124
+ super({ url, title, proxy, thumbnail, cid, rid, rcid, client_ref });
125
+ this.mp3_url = mp3_url;
126
+ this.mp3_thumbnail = mp3_thumbnail;
127
+ this._defaultFilenameSuffix = 'media';
128
+ }
129
+
130
+ get mp4_url() { return this.url; }
131
+ set mp4_url(v) { this.url = v; }
132
+
133
+ get mp4_thumbnail() { return this.thumbnail; }
134
+ set mp4_thumbnail(v) { this.thumbnail = v; }
135
+
136
+ _getUrlForHash() {
137
+ return this.url || this.mp3_url;
138
+ }
139
+
140
+ async _performSave(savePath, filename, verbose, downloadType = 'both') {
141
+ const results = {};
142
+ const tasks = [];
143
+
144
+ if ((downloadType === 'audio' || downloadType === 'both') && this.mp3_url) {
145
+ tasks.push(this._downloadWithPolling(this.mp3_url, savePath, filename, '.mp3', verbose, 'audio'));
146
+ if (this.mp3_thumbnail) {
147
+ tasks.push(this._downloadThumbnail(this.mp3_thumbnail, savePath, filename + '_audio_thumb', verbose, 'audio_thumbnail'));
148
+ }
149
+ }
150
+
151
+ if ((downloadType === 'video' || downloadType === 'both') && this.url) {
152
+ tasks.push(this._downloadWithPolling(this.url, savePath, filename, '.mp4', verbose, 'video'));
153
+ if (this.thumbnail) {
154
+ tasks.push(this._downloadThumbnail(this.thumbnail, savePath, filename + '_video_thumb', verbose, 'video_thumbnail'));
155
+ }
156
+ }
157
+
158
+ const downloaded = await Promise.all(tasks);
159
+ for (const [key, filePath] of downloaded) {
160
+ results[key] = filePath;
161
+ }
162
+ return results;
163
+ }
164
+
165
+ async _downloadWithPolling(url, savePath, filename, ext, verbose, key) {
166
+ while (true) {
167
+ const filePath = await this._downloadFile(url, savePath, filename, ext, verbose);
168
+ if (filePath === '206') {
169
+ if (verbose) console.info(`Media (${key}) still generating (206), retrying in 10s...`);
170
+ await sleep(10000);
171
+ } else {
172
+ return [key, filePath];
173
+ }
174
+ }
175
+ }
176
+
177
+ async _downloadThumbnail(url, savePath, filename, verbose, key) {
178
+ try {
179
+ const filePath = await this._downloadFile(url, savePath, filename, '.jpg', verbose);
180
+ return [key, filePath];
181
+ } catch (e) {
182
+ if (verbose) console.warn(`Failed to save thumbnail (${key}): ${e.message}`);
183
+ return [key, null];
184
+ }
185
+ }
186
+
187
+ repr() {
188
+ const urls = [];
189
+ if (this.url) urls.push(`mp4=${this.url}`);
190
+ if (this.mp3_url) urls.push(`mp3=${this.mp3_url}`);
191
+ return `GeneratedMedia(title=${this.title}, urls=${urls.join(', ')})`;
192
+ }
193
+ }
194
+
195
+ module.exports = { Video, GeneratedVideo, GeneratedMedia };