@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.
@@ -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
+ });