gemini-reverse 1.0.2 → 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/README.md +139 -154
- package/client.js +668 -152
- package/components/chatMixin.js +129 -70
- package/components/index.js +2 -1
- package/components/researchMixin.js +202 -0
- package/constants.js +157 -36
- package/index.d.ts +254 -46
- package/index.js +35 -5
- package/package.json +1 -1
- package/types/availablemodel.js +76 -0
- package/types/candidate.js +31 -17
- package/types/chathistory.js +36 -0
- package/types/chatinfo.js +23 -0
- package/types/index.js +26 -2
- package/types/modeloutput.js +4 -1
- package/types/research.js +65 -0
- package/types/researchresult.js +21 -0
- package/types/video.js +195 -0
- package/utils/accessToken.js +15 -7
- package/utils/index.js +22 -3
- package/utils/parsing.js +67 -38
- package/utils/research.js +175 -0
package/index.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { GeminiClient, ChatSession } = require('./client');
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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.
|
|
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 };
|
package/types/candidate.js
CHANGED
|
@@ -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(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"')
|
|
12
|
+
.replace(/'/g, "'")
|
|
13
|
+
.replace(/'/g, "'");
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
class Candidate {
|
|
6
|
-
constructor({
|
|
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 =
|
|
30
|
+
this.text = decodeHtml(text);
|
|
9
31
|
this.text_delta = text_delta;
|
|
10
|
-
this.thoughts = thoughts ?
|
|
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
|
-
|
|
17
|
-
if (!str) return str;
|
|
18
|
-
return str
|
|
19
|
-
.replace(/&/g, '&')
|
|
20
|
-
.replace(/</g, '<')
|
|
21
|
-
.replace(/>/g, '>')
|
|
22
|
-
.replace(/"/g, '"')
|
|
23
|
-
.replace(/'/g, '\'')
|
|
24
|
-
.replace(/'/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 <=
|
|
35
|
-
return `Candidate(rcid
|
|
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 {
|
|
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 = {
|
|
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
|
+
};
|
package/types/modeloutput.js
CHANGED
|
@@ -12,6 +12,9 @@ class ModelOutput {
|
|
|
12
12
|
get thoughts() { return this.candidates[this.chosen].thoughts; }
|
|
13
13
|
get thoughts_delta() { return this.candidates[this.chosen].thoughts_delta || ''; }
|
|
14
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; }
|
|
15
18
|
get rcid() { return this.candidates[this.chosen].rcid; }
|
|
16
19
|
|
|
17
20
|
toString() { return this.text; }
|
|
@@ -21,4 +24,4 @@ class ModelOutput {
|
|
|
21
24
|
}
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
module.exports = { ModelOutput };
|
|
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 };
|
package/utils/accessToken.js
CHANGED
|
@@ -24,8 +24,12 @@ function parseCookies(headers, base = {}) {
|
|
|
24
24
|
|
|
25
25
|
function parseProxy(str) {
|
|
26
26
|
if (!str) return undefined;
|
|
27
|
-
try {
|
|
28
|
-
|
|
27
|
+
try {
|
|
28
|
+
const u = new URL(str);
|
|
29
|
+
return { protocol: u.protocol.replace(':', ''), host: u.hostname, port: parseInt(u.port) };
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
function cacheDir() {
|
|
@@ -44,8 +48,10 @@ async function sendInitRequest(cookies, proxy = null) {
|
|
|
44
48
|
const snlm0e = (t.match(/"SNlM0e":\s*"(.*?)"/) || [])[1] || null;
|
|
45
49
|
const cfb2h = (t.match(/"cfb2h":\s*"(.*?)"/) || [])[1] || null;
|
|
46
50
|
const fdrfje = (t.match(/"FdrFJe":\s*"(.*?)"/) || [])[1] || null;
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
const language = (t.match(/"TuX5cc":\s*"(.*?)"/) || [])[1] || null;
|
|
52
|
+
const pushId = (t.match(/"qKIAYe":\s*"(.*?)"/) || [])[1] || null;
|
|
53
|
+
if (!snlm0e && !cfb2h && !fdrfje && !language && !pushId) throw new AuthError('Cookies invalid.');
|
|
54
|
+
return [snlm0e, cfb2h, fdrfje, language, pushId, parseCookies(res.headers, cookies)];
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
async function getAccessToken(baseCookies, proxy = null, verbose = false) {
|
|
@@ -59,8 +65,9 @@ async function getAccessToken(baseCookies, proxy = null, verbose = false) {
|
|
|
59
65
|
const jar = (base, extra = {}) => ({ ...extraCookies, ...base, ...extra });
|
|
60
66
|
const dir = cacheDir();
|
|
61
67
|
|
|
62
|
-
if (baseCookies['__Secure-1PSID'])
|
|
68
|
+
if (baseCookies['__Secure-1PSID']) {
|
|
63
69
|
tasks.push(sendInitRequest(jar(baseCookies), proxy));
|
|
70
|
+
}
|
|
64
71
|
|
|
65
72
|
const psid = baseCookies['__Secure-1PSID'];
|
|
66
73
|
if (psid) {
|
|
@@ -84,8 +91,9 @@ async function getAccessToken(baseCookies, proxy = null, verbose = false) {
|
|
|
84
91
|
if (validCaches === 0 && verbose) console.debug('Skipping cached cookies. No valid caches found.');
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
if (!tasks.length)
|
|
94
|
+
if (!tasks.length) {
|
|
88
95
|
throw new AuthError('No valid cookies available. Please pass __Secure-1PSID and optionally __Secure-1PSIDTS.');
|
|
96
|
+
}
|
|
89
97
|
|
|
90
98
|
let lastErr;
|
|
91
99
|
for (let i = 0; i < tasks.length; i++) {
|
|
@@ -102,4 +110,4 @@ async function getAccessToken(baseCookies, proxy = null, verbose = false) {
|
|
|
102
110
|
throw new AuthError(`Failed to initialize client. (Failed attempts: ${tasks.length})`);
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
module.exports = { getAccessToken, sendInitRequest, cookieStr, parseCookies, parseProxy, cacheDir };
|
|
113
|
+
module.exports = { getAccessToken, sendInitRequest, cookieStr, parseCookies, parseProxy, cacheDir };
|