@efffrida/gplayapi 0.0.1
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/LICENSE +674 -0
- package/README.md +1 -0
- package/devices/arm64_xxhdpi.properties +36 -0
- package/dist/GooglePlayApi.d.ts +88 -0
- package/dist/GooglePlayApi.d.ts.map +1 -0
- package/dist/GooglePlayApi.js +161 -0
- package/dist/GooglePlayApi.js.map +1 -0
- package/dist/generated/GooglePlay_pb.d.ts +8517 -0
- package/dist/generated/GooglePlay_pb.d.ts.map +1 -0
- package/dist/generated/GooglePlay_pb.js +1495 -0
- package/dist/generated/GooglePlay_pb.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/auth.d.ts +2 -0
- package/dist/internal/auth.d.ts.map +1 -0
- package/dist/internal/auth.js +166 -0
- package/dist/internal/auth.js.map +1 -0
- package/dist/internal/device.d.ts +2 -0
- package/dist/internal/device.d.ts.map +1 -0
- package/dist/internal/device.js +85 -0
- package/dist/internal/device.js.map +1 -0
- package/dist/internal/http.d.ts +2 -0
- package/dist/internal/http.d.ts.map +1 -0
- package/dist/internal/http.js +88 -0
- package/dist/internal/http.js.map +1 -0
- package/package.json +71 -0
- package/protobuf/GooglePlay.proto +2264 -0
- package/src/GooglePlayApi.ts +234 -0
- package/src/generated/GooglePlay_pb.ts +10561 -0
- package/src/index.ts +11 -0
- package/src/internal/auth.ts +220 -0
- package/src/internal/device.ts +96 -0
- package/src/internal/http.ts +194 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unofficial Google Play Store API for downloading APKs directly from Google
|
|
3
|
+
* Play Store.
|
|
4
|
+
*
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type * as HttpClientError from "@effect/platform/HttpClientError";
|
|
9
|
+
|
|
10
|
+
import * as FetchHttpClient from "@effect/platform/FetchHttpClient";
|
|
11
|
+
import * as FileSystem from "@effect/platform/FileSystem";
|
|
12
|
+
import * as HttpClient from "@effect/platform/HttpClient";
|
|
13
|
+
import * as HttpClientRequest from "@effect/platform/HttpClientRequest";
|
|
14
|
+
import * as HttpClientResponse from "@effect/platform/HttpClientResponse";
|
|
15
|
+
import * as Array from "effect/Array";
|
|
16
|
+
import * as Effect from "effect/Effect";
|
|
17
|
+
import * as Function from "effect/Function";
|
|
18
|
+
import * as Layer from "effect/Layer";
|
|
19
|
+
import * as Stream from "effect/Stream";
|
|
20
|
+
import * as url from "node:url";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
BulkDetailsRequestSchema,
|
|
24
|
+
type BulkDetailsResponse,
|
|
25
|
+
type BuyResponse,
|
|
26
|
+
type DetailsResponse,
|
|
27
|
+
} from "./generated/GooglePlay_pb.js";
|
|
28
|
+
import { makeHttpClient } from "./internal/auth.js";
|
|
29
|
+
import { Device } from "./internal/device.js";
|
|
30
|
+
import { decodeResponseFromResponseWrapper, encodeRequest } from "./internal/http.js";
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
/**
|
|
34
|
+
* @since 1.0.0
|
|
35
|
+
* @category Auth
|
|
36
|
+
*/
|
|
37
|
+
makeHttpClient,
|
|
38
|
+
} from "./internal/auth.ts";
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
/**
|
|
42
|
+
* @since 1.0.0
|
|
43
|
+
* @category Device
|
|
44
|
+
*/
|
|
45
|
+
Device,
|
|
46
|
+
} from "./internal/device.js";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @since 1.0.0
|
|
50
|
+
* @category Auth
|
|
51
|
+
*/
|
|
52
|
+
export const defaultHttpClient = Function.pipe(
|
|
53
|
+
Device.fromPropertiesFile(url.fileURLToPath(new URL("../devices/arm64_xxhdpi.properties", import.meta.url))),
|
|
54
|
+
Effect.map(makeHttpClient),
|
|
55
|
+
Layer.unwrapEffect,
|
|
56
|
+
Layer.provide(FetchHttpClient.layer)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @since 1.0.0
|
|
61
|
+
* @category API
|
|
62
|
+
*/
|
|
63
|
+
export const details = (
|
|
64
|
+
bundleIdentifier: string
|
|
65
|
+
): Effect.Effect<DetailsResponse, HttpClientError.HttpClientError, HttpClient.HttpClient> =>
|
|
66
|
+
Effect.flatMap(
|
|
67
|
+
HttpClient.get("/fdfe/details", { urlParams: { doc: bundleIdentifier } }),
|
|
68
|
+
decodeResponseFromResponseWrapper("detailsResponse")
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @since 1.0.0
|
|
73
|
+
* @category API
|
|
74
|
+
*/
|
|
75
|
+
export const bulkDetails = (
|
|
76
|
+
bundleIdentifier: string
|
|
77
|
+
): Effect.Effect<BulkDetailsResponse, HttpClientError.HttpClientError, HttpClient.HttpClient> =>
|
|
78
|
+
Function.pipe(
|
|
79
|
+
HttpClientRequest.post("/fdfe/bulkDetails"),
|
|
80
|
+
encodeRequest(BulkDetailsRequestSchema, {
|
|
81
|
+
includeDetails: true,
|
|
82
|
+
includeChildDocs: true,
|
|
83
|
+
DocId: [bundleIdentifier],
|
|
84
|
+
}),
|
|
85
|
+
Effect.flatMap(HttpClient.execute),
|
|
86
|
+
Effect.flatMap(decodeResponseFromResponseWrapper("bulkDetailsResponse"))
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @since 1.0.0
|
|
91
|
+
* @category API
|
|
92
|
+
*/
|
|
93
|
+
export const purchase = (
|
|
94
|
+
bundleIdentifier: string,
|
|
95
|
+
options: {
|
|
96
|
+
offerType: number;
|
|
97
|
+
versionCode: number | bigint;
|
|
98
|
+
certificateHash?: string | undefined;
|
|
99
|
+
}
|
|
100
|
+
): Effect.Effect<BuyResponse, HttpClientError.HttpClientError, HttpClient.HttpClient> =>
|
|
101
|
+
Effect.flatMap(
|
|
102
|
+
HttpClient.post("/fdfe/purchase", {
|
|
103
|
+
urlParams: {
|
|
104
|
+
doc: bundleIdentifier,
|
|
105
|
+
ch: options.certificateHash ?? "",
|
|
106
|
+
ot: options.offerType.toString(),
|
|
107
|
+
vc: options.versionCode.toString(),
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
decodeResponseFromResponseWrapper("buyResponse")
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @since 1.0.0
|
|
115
|
+
* @category API
|
|
116
|
+
*/
|
|
117
|
+
export const delivery = (
|
|
118
|
+
bundleIdentifier: string,
|
|
119
|
+
options: {
|
|
120
|
+
offerType: number;
|
|
121
|
+
deliveryToken: string;
|
|
122
|
+
versionCode: number | bigint;
|
|
123
|
+
certificateHash?: string | undefined;
|
|
124
|
+
}
|
|
125
|
+
) =>
|
|
126
|
+
Effect.flatMap(
|
|
127
|
+
HttpClient.get("/fdfe/delivery", {
|
|
128
|
+
urlParams: {
|
|
129
|
+
doc: bundleIdentifier,
|
|
130
|
+
dtok: options.deliveryToken,
|
|
131
|
+
ch: options.certificateHash ?? "",
|
|
132
|
+
ot: options.offerType.toString(),
|
|
133
|
+
vc: options.versionCode.toString(),
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
decodeResponseFromResponseWrapper("deliveryResponse")
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @since 1.0.0
|
|
141
|
+
* @category API
|
|
142
|
+
*/
|
|
143
|
+
export const download = Effect.fnUntraced(function* (bundleIdentifier: string) {
|
|
144
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
145
|
+
|
|
146
|
+
const { item } = yield* details(bundleIdentifier);
|
|
147
|
+
const { encodedDeliveryToken } = yield* purchase(bundleIdentifier, {
|
|
148
|
+
offerType: item?.offer[0].offerType ?? 1,
|
|
149
|
+
versionCode: item?.details?.appDetails?.versionCode ?? 0,
|
|
150
|
+
});
|
|
151
|
+
const deliveryResult = yield* delivery(bundleIdentifier, {
|
|
152
|
+
deliveryToken: encodedDeliveryToken,
|
|
153
|
+
offerType: item?.offer[0].offerType ?? 1,
|
|
154
|
+
versionCode: item?.details?.appDetails?.versionCode ?? 0,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const mainDeliveryData = deliveryResult?.appDeliveryData;
|
|
158
|
+
if (mainDeliveryData === undefined) {
|
|
159
|
+
return yield* Effect.fail(new Error("No delivery data available"));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type Result = {
|
|
163
|
+
url: string;
|
|
164
|
+
name: string;
|
|
165
|
+
file: string;
|
|
166
|
+
size: number | bigint;
|
|
167
|
+
integrity: { sha1: string } | { sha256: string };
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const main = Effect.gen(function* () {
|
|
171
|
+
const file = yield* fileSystem.makeTempFileScoped({ suffix: ".apk" });
|
|
172
|
+
|
|
173
|
+
const stream = HttpClientResponse.stream(HttpClient.get(mainDeliveryData.downloadUrl));
|
|
174
|
+
yield* Stream.run(stream, fileSystem.sink(file));
|
|
175
|
+
|
|
176
|
+
yield* Effect.logDebug(
|
|
177
|
+
`main APK of ${mainDeliveryData.downloadSize}bytes from ${mainDeliveryData.downloadUrl} with integrity ${mainDeliveryData.sha256}(sha256) downloaded to ${file}`
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
file,
|
|
182
|
+
name: `${bundleIdentifier}.apk`,
|
|
183
|
+
url: mainDeliveryData.downloadUrl,
|
|
184
|
+
size: mainDeliveryData.downloadSize,
|
|
185
|
+
integrity: { sha256: mainDeliveryData.sha256 },
|
|
186
|
+
} satisfies Result;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const splits = Array.map(mainDeliveryData.splitDeliveryData, (split) =>
|
|
190
|
+
Effect.gen(function* () {
|
|
191
|
+
const file = yield* fileSystem.makeTempFileScoped({ suffix: ".apk" });
|
|
192
|
+
|
|
193
|
+
const stream = HttpClientResponse.stream(HttpClient.get(split.downloadUrl));
|
|
194
|
+
yield* Stream.run(stream, fileSystem.sink(file));
|
|
195
|
+
|
|
196
|
+
yield* Effect.logDebug(
|
|
197
|
+
`split ${split.name} of ${split.downloadSize}bytes from ${split.downloadUrl} with integrity ${split.sha256}(sha256) downloaded to ${file}`
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
file,
|
|
202
|
+
url: split.downloadUrl,
|
|
203
|
+
size: split.downloadSize,
|
|
204
|
+
name: `${split.name}.apk`,
|
|
205
|
+
integrity: { sha256: split.sha256 },
|
|
206
|
+
} satisfies Result;
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const expansions = Array.map(mainDeliveryData.additionalFile, (expansion) =>
|
|
211
|
+
Effect.gen(function* () {
|
|
212
|
+
const typeStr = expansion.fileType === 1 ? "main" : "patch";
|
|
213
|
+
const name = `${typeStr}.${expansion.versionCode}.${bundleIdentifier}.obb`;
|
|
214
|
+
const file = yield* fileSystem.makeTempFileScoped({ suffix: ".obb" });
|
|
215
|
+
|
|
216
|
+
const stream = HttpClientResponse.stream(HttpClient.get(expansion.downloadUrl));
|
|
217
|
+
yield* Stream.run(stream, fileSystem.sink(file));
|
|
218
|
+
|
|
219
|
+
yield* Effect.logDebug(
|
|
220
|
+
`expansion file ${name} of ${expansion.size}bytes from ${expansion.downloadUrl} with integrity ${expansion.sha1}(sha1) downloaded to ${file}`
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
file,
|
|
225
|
+
name,
|
|
226
|
+
size: expansion.size,
|
|
227
|
+
url: expansion.downloadUrl,
|
|
228
|
+
integrity: { sha1: expansion.sha1 },
|
|
229
|
+
} satisfies Result;
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return yield* Effect.all([main, ...splits, ...expansions]);
|
|
234
|
+
});
|