pabal-store-api-mcp 1.3.11 → 1.3.14
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/dist/src/core/services/google-play-service.d.ts +4 -0
- package/dist/src/core/services/google-play-service.js +36 -7
- package/dist/src/index.js +1 -1
- package/dist/src/packages/stores/app-store/client.d.ts +21 -2
- package/dist/src/packages/stores/app-store/client.js +83 -13
- package/dist/src/packages/stores/play-store/client.d.ts +2 -0
- package/dist/src/packages/stores/play-store/client.js +130 -97
- package/dist/src/tools/aso/push.js +4 -1
- package/package.json +1 -1
|
@@ -10,6 +10,10 @@ export declare function resolveGooglePlayLocales(allLocales: string[], requested
|
|
|
10
10
|
localesToPush: string[];
|
|
11
11
|
missingLocales: string[];
|
|
12
12
|
};
|
|
13
|
+
export declare function shouldPushGooglePlayAppDetails({ hasContactDetails, requestedLocales, }: {
|
|
14
|
+
hasContactDetails: boolean;
|
|
15
|
+
requestedLocales?: string[];
|
|
16
|
+
}): boolean;
|
|
13
17
|
/**
|
|
14
18
|
* Google Play-facing service layer that wraps client creation and common operations.
|
|
15
19
|
* Keeps MCP tools independent from client factories and SDK details.
|
|
@@ -6,6 +6,7 @@ import { verifyPlayStoreAuth } from "../../packages/stores/play-store/verify-aut
|
|
|
6
6
|
import { createGooglePlayClient } from "../../core/clients/google-play-factory.js";
|
|
7
7
|
import { parseGooglePlayScreenshots, hasScreenshots, } from "../../core/helpers/screenshot-helpers.js";
|
|
8
8
|
import { checkPushPrerequisites, serviceFailure, toServiceResult, updateRegisteredLocales, } from "./service-helpers.js";
|
|
9
|
+
const GOOGLE_PLAY_SCREENSHOT_LOCALE_BATCH_SIZE = 5;
|
|
9
10
|
export function resolveGooglePlayLocales(allLocales, requestedLocales) {
|
|
10
11
|
if (!requestedLocales?.length) {
|
|
11
12
|
return { localesToPush: allLocales, missingLocales: [] };
|
|
@@ -16,6 +17,9 @@ export function resolveGooglePlayLocales(allLocales, requestedLocales) {
|
|
|
16
17
|
missingLocales: requestedLocales.filter((locale) => !allLocales.includes(locale)),
|
|
17
18
|
};
|
|
18
19
|
}
|
|
20
|
+
export function shouldPushGooglePlayAppDetails({ hasContactDetails, requestedLocales, }) {
|
|
21
|
+
return hasContactDetails && !requestedLocales?.length;
|
|
22
|
+
}
|
|
19
23
|
/**
|
|
20
24
|
* Google Play-facing service layer that wraps client creation and common operations.
|
|
21
25
|
* Keeps MCP tools independent from client factories and SDK details.
|
|
@@ -197,8 +201,13 @@ export class GooglePlayService {
|
|
|
197
201
|
googlePlayData.locales[locale],
|
|
198
202
|
])),
|
|
199
203
|
});
|
|
200
|
-
// Push app-level contact information
|
|
201
|
-
|
|
204
|
+
// Push app-level contact information once for full pushes. Partial locale
|
|
205
|
+
// pushes are commonly batched, and repeating details edits can invalidate
|
|
206
|
+
// otherwise-successful listing commits on Google Play.
|
|
207
|
+
if (shouldPushGooglePlayAppDetails({
|
|
208
|
+
hasContactDetails: Boolean(googlePlayData.contactEmail || googlePlayData.contactWebsite),
|
|
209
|
+
requestedLocales: locales,
|
|
210
|
+
})) {
|
|
202
211
|
console.error(`[GooglePlay] 📤 Pushing app details...`);
|
|
203
212
|
await client.pushAppDetails({
|
|
204
213
|
contactEmail: googlePlayData.contactEmail,
|
|
@@ -206,6 +215,9 @@ export class GooglePlayService {
|
|
|
206
215
|
});
|
|
207
216
|
console.error(`[GooglePlay] ✅ App details uploaded successfully`);
|
|
208
217
|
}
|
|
218
|
+
else if (locales?.length) {
|
|
219
|
+
console.error(`[GooglePlay] ⏭️ Skipping app details for partial locale push`);
|
|
220
|
+
}
|
|
209
221
|
// Upload screenshots if enabled
|
|
210
222
|
if (uploadImages && slug) {
|
|
211
223
|
console.error(`[GooglePlay] 📤 Uploading screenshots...`);
|
|
@@ -214,6 +226,7 @@ export class GooglePlayService {
|
|
|
214
226
|
const uploadedLocales = [];
|
|
215
227
|
const skippedLocales = [];
|
|
216
228
|
const failedLocales = [];
|
|
229
|
+
const screenshotUploadOptions = [];
|
|
217
230
|
for (const locale of localesToPush) {
|
|
218
231
|
try {
|
|
219
232
|
const localeData = googlePlayData.locales[locale];
|
|
@@ -271,11 +284,11 @@ export class GooglePlayService {
|
|
|
271
284
|
skippedLocales.push(locale);
|
|
272
285
|
continue;
|
|
273
286
|
}
|
|
274
|
-
console.error(`[GooglePlay]
|
|
287
|
+
console.error(`[GooglePlay] 📋 Queued screenshots for ${locale} (batch mode - will replace existing)...`);
|
|
275
288
|
// Google Play upload strategy:
|
|
276
289
|
// - phone → uploads to phoneScreenshots AND sevenInchScreenshots (both use same images)
|
|
277
290
|
// - tablet → uploads to tenInchScreenshots only
|
|
278
|
-
|
|
291
|
+
screenshotUploadOptions.push({
|
|
279
292
|
language: locale,
|
|
280
293
|
phoneScreenshots: screenshots.phone,
|
|
281
294
|
sevenInchScreenshots: screenshots.phone,
|
|
@@ -283,14 +296,28 @@ export class GooglePlayService {
|
|
|
283
296
|
featureGraphic: screenshots.featureGraphic || undefined,
|
|
284
297
|
imageUploadTimeoutMs,
|
|
285
298
|
});
|
|
286
|
-
console.error(`[GooglePlay] ✅ Images uploaded for ${locale}: ${uploadResult.uploaded.phoneScreenshots} phone, ${uploadResult.uploaded.sevenInchScreenshots} 7-inch, ${uploadResult.uploaded.tenInchScreenshots} 10-inch, feature graphic ${uploadResult.uploaded.featureGraphic ? "yes" : "no"}`);
|
|
287
|
-
uploadedLocales.push(locale);
|
|
288
299
|
}
|
|
289
300
|
catch (error) {
|
|
290
301
|
console.error(`[GooglePlay] ❌ Failed to upload screenshots for ${locale}: ${error instanceof Error ? error.message : String(error)}`);
|
|
291
302
|
failedLocales.push(locale);
|
|
292
303
|
}
|
|
293
304
|
}
|
|
305
|
+
for (let offset = 0; offset < screenshotUploadOptions.length; offset += GOOGLE_PLAY_SCREENSHOT_LOCALE_BATCH_SIZE) {
|
|
306
|
+
const batch = screenshotUploadOptions.slice(offset, offset + GOOGLE_PLAY_SCREENSHOT_LOCALE_BATCH_SIZE);
|
|
307
|
+
try {
|
|
308
|
+
console.error(`[GooglePlay] 📤 Uploading screenshots for ${batch.length} locale(s) in one edit...`);
|
|
309
|
+
const uploadResults = await client.uploadScreenshotsForLocales(batch);
|
|
310
|
+
for (const uploadResult of uploadResults) {
|
|
311
|
+
console.error(`[GooglePlay] ✅ Images uploaded for ${uploadResult.language}: ${uploadResult.uploaded.phoneScreenshots} phone, ${uploadResult.uploaded.sevenInchScreenshots} 7-inch, ${uploadResult.uploaded.tenInchScreenshots} 10-inch, feature graphic ${uploadResult.uploaded.featureGraphic ? "yes" : "no"}`);
|
|
312
|
+
uploadedLocales.push(uploadResult.language);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
const failedBatchLocales = batch.map((option) => option.language);
|
|
317
|
+
console.error(`[GooglePlay] ❌ Batch screenshot upload failed for ${failedBatchLocales.join(", ")}: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
|
+
failedLocales.push(...failedBatchLocales);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
294
321
|
console.error(`[GooglePlay] 📊 Screenshot upload summary: ${uploadedLocales.length} succeeded, ${skippedLocales.length} skipped, ${failedLocales.length} failed`);
|
|
295
322
|
if (uploadedLocales.length > 0) {
|
|
296
323
|
console.error(`[GooglePlay] ✅ Uploaded: ${uploadedLocales.join(", ")}`);
|
|
@@ -299,7 +326,9 @@ export class GooglePlayService {
|
|
|
299
326
|
console.error(`[GooglePlay] ⏭️ Skipped: ${skippedLocales.join(", ")}`);
|
|
300
327
|
}
|
|
301
328
|
if (failedLocales.length > 0) {
|
|
302
|
-
|
|
329
|
+
const uniqueFailedLocales = [...new Set(failedLocales)];
|
|
330
|
+
console.error(`[GooglePlay] ❌ Failed: ${uniqueFailedLocales.join(", ")}`);
|
|
331
|
+
throw new Error(`Screenshot upload failed for locales: ${uniqueFailedLocales.join(", ")}`);
|
|
303
332
|
}
|
|
304
333
|
}
|
|
305
334
|
try {
|
package/dist/src/index.js
CHANGED
|
@@ -204,7 +204,7 @@ registerToolWithInfo("aso-push", {
|
|
|
204
204
|
.int()
|
|
205
205
|
.positive()
|
|
206
206
|
.optional()
|
|
207
|
-
.describe("Per-image upload timeout in milliseconds"),
|
|
207
|
+
.describe("Per-image upload timeout in milliseconds (default: 600000 when uploadImages=true)"),
|
|
208
208
|
dryRun: z
|
|
209
209
|
.boolean()
|
|
210
210
|
.optional()
|
|
@@ -108,15 +108,34 @@ export declare class AppStoreClient {
|
|
|
108
108
|
* Delete all screenshots in a screenshot set
|
|
109
109
|
*/
|
|
110
110
|
private deleteAllScreenshotsInSet;
|
|
111
|
+
private deleteScreenshots;
|
|
112
|
+
private prepareScreenshotSetForUpload;
|
|
111
113
|
private sanitizePathSegment;
|
|
112
114
|
private getScreenshotLockPath;
|
|
113
115
|
private acquireScreenshotUploadLock;
|
|
114
116
|
private preflightScreenshotBatch;
|
|
115
117
|
/**
|
|
116
|
-
* Upload multiple screenshots for a locale, replacing existing ones
|
|
118
|
+
* Upload multiple screenshots for a locale, replacing existing ones.
|
|
119
|
+
*
|
|
120
|
+
* Failure model:
|
|
121
|
+
*
|
|
122
|
+
* existing screenshots ──┐
|
|
123
|
+
* ├─ free only the slots App Store requires
|
|
124
|
+
* incoming screenshots ──┘
|
|
125
|
+
* │
|
|
126
|
+
* ├─ upload + commit every incoming screenshot
|
|
127
|
+
* │
|
|
128
|
+
* └─ only after success, delete the remaining old screenshots
|
|
129
|
+
*
|
|
130
|
+
* This avoids the old delete-first behavior where a timeout could leave the
|
|
131
|
+
* user with an empty screenshot set. If upload fails, most existing
|
|
132
|
+
* screenshots remain visible and any successfully uploaded new screenshots
|
|
133
|
+
* are left in App Store Connect for manual or retry cleanup.
|
|
134
|
+
*
|
|
117
135
|
* 1. Find or create screenshot sets for each display type
|
|
118
|
-
* 2. Delete existing screenshots
|
|
136
|
+
* 2. Delete only enough existing screenshots to fit the incoming batch
|
|
119
137
|
* 3. Upload new screenshots in order
|
|
138
|
+
* 4. Delete remaining old screenshots after the batch succeeds
|
|
120
139
|
*/
|
|
121
140
|
uploadScreenshotsForLocale(options: {
|
|
122
141
|
locale: string;
|
|
@@ -12,6 +12,9 @@ import { DEFAULT_LOCALE } from "../../../packages/configs/aso-config/constants.j
|
|
|
12
12
|
import { getAsoDir } from "../../../packages/configs/aso-config/utils.js";
|
|
13
13
|
import { convertToAsoData, convertToMultilingualAsoData, convertToReleaseNote, fetchScreenshotsForLocalization, mapLocalizationsByLocale, selectEnglishAppName, sortReleaseNotes, sortVersions, } from "./api-converters.js";
|
|
14
14
|
import { APP_STORE_API_BASE_URL, APP_STORE_PLATFORM, DEFAULT_APP_LIST_LIMIT, DEFAULT_VERSIONS_FETCH_LIMIT, } from "./constants.js";
|
|
15
|
+
const APP_STORE_SCREENSHOT_SET_MAX_COUNT = 10;
|
|
16
|
+
const DEFAULT_IMAGE_UPLOAD_TIMEOUT_MS = 10 * 60 * 1000;
|
|
17
|
+
const SCREENSHOT_UPLOAD_LOCK_STALE_MS = 30 * 60 * 1000;
|
|
15
18
|
export class AppStoreClient {
|
|
16
19
|
issuerId;
|
|
17
20
|
keyId;
|
|
@@ -727,9 +730,10 @@ export class AppStoreClient {
|
|
|
727
730
|
}
|
|
728
731
|
});
|
|
729
732
|
req.on("error", reject);
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
+
const effectiveTimeoutMs = timeoutMs ?? DEFAULT_IMAGE_UPLOAD_TIMEOUT_MS;
|
|
734
|
+
if (effectiveTimeoutMs > 0) {
|
|
735
|
+
req.setTimeout(effectiveTimeoutMs, () => {
|
|
736
|
+
req.destroy(new Error(`Upload timed out after ${effectiveTimeoutMs}ms`));
|
|
733
737
|
});
|
|
734
738
|
}
|
|
735
739
|
req.write(fileBuffer);
|
|
@@ -779,13 +783,47 @@ export class AppStoreClient {
|
|
|
779
783
|
async deleteAllScreenshotsInSet(screenshotSetId) {
|
|
780
784
|
const screenshotsResponse = await this.listScreenshots(screenshotSetId);
|
|
781
785
|
const screenshots = screenshotsResponse.data || [];
|
|
786
|
+
return this.deleteScreenshots(screenshots);
|
|
787
|
+
}
|
|
788
|
+
async deleteScreenshots(screenshots) {
|
|
782
789
|
let deletedCount = 0;
|
|
783
790
|
for (const screenshot of screenshots) {
|
|
791
|
+
if (screenshot.type !== "appScreenshots") {
|
|
792
|
+
console.error(`[AppStore] Skipping non-screenshot asset ${screenshot.id}`);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
784
795
|
await this.deleteScreenshot(screenshot.id);
|
|
785
796
|
deletedCount++;
|
|
786
797
|
}
|
|
787
798
|
return deletedCount;
|
|
788
799
|
}
|
|
800
|
+
async prepareScreenshotSetForUpload(screenshotSetId, incomingCount) {
|
|
801
|
+
if (incomingCount > APP_STORE_SCREENSHOT_SET_MAX_COUNT) {
|
|
802
|
+
throw new Error(`Preflight failed: App Store allows up to ${APP_STORE_SCREENSHOT_SET_MAX_COUNT} screenshots per display type, got ${incomingCount}`);
|
|
803
|
+
}
|
|
804
|
+
const screenshotsResponse = await this.listScreenshots(screenshotSetId);
|
|
805
|
+
const existingScreenshots = (screenshotsResponse.data || []).filter((screenshot) => screenshot.type === "appScreenshots");
|
|
806
|
+
const nonScreenshotCount = (screenshotsResponse.data || []).length - existingScreenshots.length;
|
|
807
|
+
if (nonScreenshotCount > 0) {
|
|
808
|
+
console.error(`[AppStore] Ignoring ${nonScreenshotCount} non-screenshot asset(s) in screenshot set`);
|
|
809
|
+
}
|
|
810
|
+
const slotsToFree = Math.max(0, existingScreenshots.length +
|
|
811
|
+
incomingCount -
|
|
812
|
+
APP_STORE_SCREENSHOT_SET_MAX_COUNT);
|
|
813
|
+
// Preserve the first screenshots as long as possible. If the upload fails
|
|
814
|
+
// after freeing slots, users are more likely to still see the primary
|
|
815
|
+
// screenshots rather than the tail of the old set.
|
|
816
|
+
const screenshotsToDeleteBeforeUpload = slotsToFree > 0 ? existingScreenshots.slice(-slotsToFree) : [];
|
|
817
|
+
const screenshotsToDeleteAfterUpload = existingScreenshots.slice(0, existingScreenshots.length - slotsToFree);
|
|
818
|
+
const deletedBeforeUpload = await this.deleteScreenshots(screenshotsToDeleteBeforeUpload);
|
|
819
|
+
if (deletedBeforeUpload > 0) {
|
|
820
|
+
console.error(`[AppStore] Deleted ${deletedBeforeUpload} existing screenshots to free upload slots`);
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
deletedBeforeUpload,
|
|
824
|
+
screenshotsToDeleteAfterUpload,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
789
827
|
sanitizePathSegment(value) {
|
|
790
828
|
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
791
829
|
}
|
|
@@ -804,9 +842,19 @@ export class AppStoreClient {
|
|
|
804
842
|
catch (error) {
|
|
805
843
|
const code = error.code;
|
|
806
844
|
if (code === "EEXIST") {
|
|
807
|
-
|
|
845
|
+
const lockAgeMs = Date.now() - statSync(lockPath).mtimeMs;
|
|
846
|
+
if (lockAgeMs > SCREENSHOT_UPLOAD_LOCK_STALE_MS) {
|
|
847
|
+
console.error(`[AppStore] Removing stale screenshot upload lock (${lockPath})`);
|
|
848
|
+
unlinkSync(lockPath);
|
|
849
|
+
descriptor = openSync(lockPath, "wx");
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
throw new Error(`Screenshot upload lock is already held (${lockPath}). Another upload is running for this locale/display type.`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
throw error;
|
|
808
857
|
}
|
|
809
|
-
throw error;
|
|
810
858
|
}
|
|
811
859
|
closeSync(descriptor);
|
|
812
860
|
return () => {
|
|
@@ -852,10 +900,27 @@ export class AppStoreClient {
|
|
|
852
900
|
}
|
|
853
901
|
}
|
|
854
902
|
/**
|
|
855
|
-
* Upload multiple screenshots for a locale, replacing existing ones
|
|
903
|
+
* Upload multiple screenshots for a locale, replacing existing ones.
|
|
904
|
+
*
|
|
905
|
+
* Failure model:
|
|
906
|
+
*
|
|
907
|
+
* existing screenshots ──┐
|
|
908
|
+
* ├─ free only the slots App Store requires
|
|
909
|
+
* incoming screenshots ──┘
|
|
910
|
+
* │
|
|
911
|
+
* ├─ upload + commit every incoming screenshot
|
|
912
|
+
* │
|
|
913
|
+
* └─ only after success, delete the remaining old screenshots
|
|
914
|
+
*
|
|
915
|
+
* This avoids the old delete-first behavior where a timeout could leave the
|
|
916
|
+
* user with an empty screenshot set. If upload fails, most existing
|
|
917
|
+
* screenshots remain visible and any successfully uploaded new screenshots
|
|
918
|
+
* are left in App Store Connect for manual or retry cleanup.
|
|
919
|
+
*
|
|
856
920
|
* 1. Find or create screenshot sets for each display type
|
|
857
|
-
* 2. Delete existing screenshots
|
|
921
|
+
* 2. Delete only enough existing screenshots to fit the incoming batch
|
|
858
922
|
* 3. Upload new screenshots in order
|
|
923
|
+
* 4. Delete remaining old screenshots after the batch succeeds
|
|
859
924
|
*/
|
|
860
925
|
async uploadScreenshotsForLocale(options) {
|
|
861
926
|
const { locale, screenshots, imageUploadTimeoutMs } = options;
|
|
@@ -902,13 +967,10 @@ export class AppStoreClient {
|
|
|
902
967
|
console.error(`[AppStore] Processing ${displayType} (${screenshotList.length} screenshots)...`);
|
|
903
968
|
// Find or create screenshot set
|
|
904
969
|
const screenshotSetId = await this.findOrCreateScreenshotSet(localizationId, displayType);
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
if (deletedCount > 0) {
|
|
908
|
-
console.error(`[AppStore] Deleted ${deletedCount} existing screenshots`);
|
|
909
|
-
result.deleted += deletedCount;
|
|
910
|
-
}
|
|
970
|
+
const { deletedBeforeUpload, screenshotsToDeleteAfterUpload } = await this.prepareScreenshotSetForUpload(screenshotSetId, screenshotList.length);
|
|
971
|
+
result.deleted += deletedBeforeUpload;
|
|
911
972
|
// Upload new screenshots in order
|
|
973
|
+
let uploadedForDisplayType = 0;
|
|
912
974
|
for (const screenshot of screenshotList) {
|
|
913
975
|
try {
|
|
914
976
|
const fileBuffer = readFileSync(screenshot.path);
|
|
@@ -925,12 +987,20 @@ export class AppStoreClient {
|
|
|
925
987
|
await this.commitAppScreenshot(screenshotData.id);
|
|
926
988
|
console.error(`[AppStore] ✅ ${screenshot.filename}`);
|
|
927
989
|
result.uploaded++;
|
|
990
|
+
uploadedForDisplayType++;
|
|
928
991
|
}
|
|
929
992
|
catch (error) {
|
|
930
993
|
result.failed++;
|
|
931
994
|
throw new Error(`[${displayType}] ${screenshot.filename}: ${error instanceof Error ? error.message : String(error)}`);
|
|
932
995
|
}
|
|
933
996
|
}
|
|
997
|
+
if (uploadedForDisplayType === screenshotList.length) {
|
|
998
|
+
const deletedAfterUpload = await this.deleteScreenshots(screenshotsToDeleteAfterUpload);
|
|
999
|
+
if (deletedAfterUpload > 0) {
|
|
1000
|
+
console.error(`[AppStore] Deleted ${deletedAfterUpload} replaced screenshots after successful upload`);
|
|
1001
|
+
result.deleted += deletedAfterUpload;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
934
1004
|
}
|
|
935
1005
|
finally {
|
|
936
1006
|
releaseLock();
|
|
@@ -55,6 +55,8 @@ export declare class GooglePlayClient {
|
|
|
55
55
|
* Deletes existing screenshots before uploading new ones
|
|
56
56
|
*/
|
|
57
57
|
uploadScreenshotsForLocale(options: BatchUploadScreenshotsOptions): Promise<BatchUploadScreenshotsResult>;
|
|
58
|
+
uploadScreenshotsForLocales(optionsList: BatchUploadScreenshotsOptions[]): Promise<BatchUploadScreenshotsResult[]>;
|
|
59
|
+
private uploadScreenshotsInSession;
|
|
58
60
|
private uploadImageWithOptionalTimeout;
|
|
59
61
|
private getTrack;
|
|
60
62
|
private updateTrack;
|
|
@@ -620,7 +620,6 @@ export class GooglePlayClient {
|
|
|
620
620
|
* Deletes existing screenshots before uploading new ones
|
|
621
621
|
*/
|
|
622
622
|
async uploadScreenshotsForLocale(options) {
|
|
623
|
-
const { language, phoneScreenshots = [], sevenInchScreenshots = [], tenInchScreenshots = [], featureGraphic, imageUploadTimeoutMs, } = options;
|
|
624
623
|
const authClient = await this.auth.getClient();
|
|
625
624
|
const editResponse = await this.createEdit(authClient, this.packageName);
|
|
626
625
|
const editId = editResponse.data.id;
|
|
@@ -629,6 +628,61 @@ export class GooglePlayClient {
|
|
|
629
628
|
packageName: this.packageName,
|
|
630
629
|
editId,
|
|
631
630
|
};
|
|
631
|
+
try {
|
|
632
|
+
const result = await this.uploadScreenshotsInSession(session, options);
|
|
633
|
+
// Commit all changes
|
|
634
|
+
console.error(`[GooglePlayClient] Committing screenshots for ${options.language}...`);
|
|
635
|
+
await this.commitEdit(session);
|
|
636
|
+
console.error(`[GooglePlayClient] ✅ Screenshots committed for ${options.language}`);
|
|
637
|
+
return result;
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
console.error(`[GooglePlayClient] Rolling back screenshot upload for ${options.language}...`);
|
|
641
|
+
try {
|
|
642
|
+
await this.deleteEdit(session);
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// Ignore deletion failure
|
|
646
|
+
}
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async uploadScreenshotsForLocales(optionsList) {
|
|
651
|
+
if (optionsList.length === 0)
|
|
652
|
+
return [];
|
|
653
|
+
const authClient = await this.auth.getClient();
|
|
654
|
+
const editResponse = await this.createEdit(authClient, this.packageName);
|
|
655
|
+
const editId = editResponse.data.id;
|
|
656
|
+
const session = {
|
|
657
|
+
auth: authClient,
|
|
658
|
+
packageName: this.packageName,
|
|
659
|
+
editId,
|
|
660
|
+
};
|
|
661
|
+
try {
|
|
662
|
+
const results = [];
|
|
663
|
+
for (const options of optionsList) {
|
|
664
|
+
console.error(`[GooglePlayClient] Preparing screenshots for ${options.language}...`);
|
|
665
|
+
const result = await this.uploadScreenshotsInSession(session, options);
|
|
666
|
+
results.push(result);
|
|
667
|
+
}
|
|
668
|
+
console.error(`[GooglePlayClient] Committing screenshots for ${optionsList.length} locale(s)...`);
|
|
669
|
+
await this.commitEdit(session);
|
|
670
|
+
console.error(`[GooglePlayClient] ✅ Screenshots committed for ${optionsList.length} locale(s)`);
|
|
671
|
+
return results;
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
console.error(`[GooglePlayClient] Rolling back batch screenshot upload...`);
|
|
675
|
+
try {
|
|
676
|
+
await this.deleteEdit(session);
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
// Ignore deletion failure
|
|
680
|
+
}
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async uploadScreenshotsInSession(session, options) {
|
|
685
|
+
const { language, phoneScreenshots = [], sevenInchScreenshots = [], tenInchScreenshots = [], featureGraphic, imageUploadTimeoutMs, } = options;
|
|
632
686
|
const result = {
|
|
633
687
|
language,
|
|
634
688
|
uploaded: {
|
|
@@ -638,115 +692,94 @@ export class GooglePlayClient {
|
|
|
638
692
|
featureGraphic: false,
|
|
639
693
|
},
|
|
640
694
|
};
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
try {
|
|
646
|
-
await this.deleteAllImages(session, language, "phoneScreenshots");
|
|
647
|
-
}
|
|
648
|
-
catch (e) {
|
|
649
|
-
// Ignore if no images exist
|
|
650
|
-
if (e.code !== 404) {
|
|
651
|
-
console.error(`[GooglePlayClient] Warning: Failed to delete phone screenshots: ${e.message}`);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
695
|
+
if (phoneScreenshots.length > 0) {
|
|
696
|
+
console.error(`[GooglePlayClient] Deleting existing phone screenshots for ${language}...`);
|
|
697
|
+
try {
|
|
698
|
+
await this.deleteAllImages(session, language, "phoneScreenshots");
|
|
654
699
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
catch (e) {
|
|
661
|
-
if (e.code !== 404) {
|
|
662
|
-
console.error(`[GooglePlayClient] Warning: Failed to delete 7-inch screenshots: ${e.message}`);
|
|
663
|
-
}
|
|
700
|
+
catch (e) {
|
|
701
|
+
// Ignore if no images exist
|
|
702
|
+
if (e.code !== 404) {
|
|
703
|
+
console.error(`[GooglePlayClient] Warning: Failed to delete phone screenshots: ${e.message}`);
|
|
664
704
|
}
|
|
665
705
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
catch (e) {
|
|
672
|
-
if (e.code !== 404) {
|
|
673
|
-
console.error(`[GooglePlayClient] Warning: Failed to delete 10-inch screenshots: ${e.message}`);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
706
|
+
}
|
|
707
|
+
if (sevenInchScreenshots.length > 0) {
|
|
708
|
+
console.error(`[GooglePlayClient] Deleting existing 7-inch screenshots for ${language}...`);
|
|
709
|
+
try {
|
|
710
|
+
await this.deleteAllImages(session, language, "sevenInchScreenshots");
|
|
676
711
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
await this.deleteAllImages(session, language, "featureGraphic");
|
|
681
|
-
}
|
|
682
|
-
catch (e) {
|
|
683
|
-
if (e.code !== 404) {
|
|
684
|
-
console.error(`[GooglePlayClient] Warning: Failed to delete feature graphic: ${e.message}`);
|
|
685
|
-
}
|
|
712
|
+
catch (e) {
|
|
713
|
+
if (e.code !== 404) {
|
|
714
|
+
console.error(`[GooglePlayClient] Warning: Failed to delete 7-inch screenshots: ${e.message}`);
|
|
686
715
|
}
|
|
687
716
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
await this.uploadImageWithOptionalTimeout(session, language, "phoneScreenshots", imageBuffer, imageUploadTimeoutMs);
|
|
698
|
-
console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
|
|
699
|
-
result.uploaded.phoneScreenshots++;
|
|
700
|
-
}
|
|
701
|
-
// Upload 7-inch tablet screenshots
|
|
702
|
-
for (let i = 0; i < sevenInchScreenshots.length; i++) {
|
|
703
|
-
const imagePath = sevenInchScreenshots[i];
|
|
704
|
-
if (!existsSync(imagePath)) {
|
|
705
|
-
console.error(`[GooglePlayClient] Warning: 7-inch screenshot not found: ${imagePath}`);
|
|
706
|
-
continue;
|
|
707
|
-
}
|
|
708
|
-
const imageBuffer = readFileSync(imagePath);
|
|
709
|
-
const fileName = imagePath.split("/").pop() || `tablet7-${i + 1}.png`;
|
|
710
|
-
await this.uploadImageWithOptionalTimeout(session, language, "sevenInchScreenshots", imageBuffer, imageUploadTimeoutMs);
|
|
711
|
-
console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
|
|
712
|
-
result.uploaded.sevenInchScreenshots++;
|
|
713
|
-
}
|
|
714
|
-
// Upload 10-inch tablet screenshots
|
|
715
|
-
for (let i = 0; i < tenInchScreenshots.length; i++) {
|
|
716
|
-
const imagePath = tenInchScreenshots[i];
|
|
717
|
-
if (!existsSync(imagePath)) {
|
|
718
|
-
console.error(`[GooglePlayClient] Warning: 10-inch screenshot not found: ${imagePath}`);
|
|
719
|
-
continue;
|
|
717
|
+
}
|
|
718
|
+
if (tenInchScreenshots.length > 0) {
|
|
719
|
+
console.error(`[GooglePlayClient] Deleting existing 10-inch screenshots for ${language}...`);
|
|
720
|
+
try {
|
|
721
|
+
await this.deleteAllImages(session, language, "tenInchScreenshots");
|
|
722
|
+
}
|
|
723
|
+
catch (e) {
|
|
724
|
+
if (e.code !== 404) {
|
|
725
|
+
console.error(`[GooglePlayClient] Warning: Failed to delete 10-inch screenshots: ${e.message}`);
|
|
720
726
|
}
|
|
721
|
-
const imageBuffer = readFileSync(imagePath);
|
|
722
|
-
const fileName = imagePath.split("/").pop() || `tablet10-${i + 1}.png`;
|
|
723
|
-
await this.uploadImageWithOptionalTimeout(session, language, "tenInchScreenshots", imageBuffer, imageUploadTimeoutMs);
|
|
724
|
-
console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
|
|
725
|
-
result.uploaded.tenInchScreenshots++;
|
|
726
|
-
}
|
|
727
|
-
// Upload feature graphic
|
|
728
|
-
if (featureGraphic && existsSync(featureGraphic)) {
|
|
729
|
-
const imageBuffer = readFileSync(featureGraphic);
|
|
730
|
-
await this.uploadImageWithOptionalTimeout(session, language, "featureGraphic", imageBuffer, imageUploadTimeoutMs);
|
|
731
|
-
console.error(`[GooglePlayClient] ✅ Uploaded feature-graphic.png`);
|
|
732
|
-
result.uploaded.featureGraphic = true;
|
|
733
727
|
}
|
|
734
|
-
// Commit all changes
|
|
735
|
-
console.error(`[GooglePlayClient] Committing screenshots for ${language}...`);
|
|
736
|
-
await this.commitEdit(session);
|
|
737
|
-
console.error(`[GooglePlayClient] ✅ Screenshots committed for ${language}`);
|
|
738
|
-
return result;
|
|
739
728
|
}
|
|
740
|
-
|
|
741
|
-
console.error(`[GooglePlayClient]
|
|
729
|
+
if (featureGraphic) {
|
|
730
|
+
console.error(`[GooglePlayClient] Deleting existing feature graphic for ${language}...`);
|
|
742
731
|
try {
|
|
743
|
-
await this.
|
|
732
|
+
await this.deleteAllImages(session, language, "featureGraphic");
|
|
744
733
|
}
|
|
745
|
-
catch {
|
|
746
|
-
|
|
734
|
+
catch (e) {
|
|
735
|
+
if (e.code !== 404) {
|
|
736
|
+
console.error(`[GooglePlayClient] Warning: Failed to delete feature graphic: ${e.message}`);
|
|
737
|
+
}
|
|
747
738
|
}
|
|
748
|
-
throw error;
|
|
749
739
|
}
|
|
740
|
+
for (let i = 0; i < phoneScreenshots.length; i++) {
|
|
741
|
+
const imagePath = phoneScreenshots[i];
|
|
742
|
+
if (!existsSync(imagePath)) {
|
|
743
|
+
console.error(`[GooglePlayClient] Warning: Phone screenshot not found: ${imagePath}`);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const imageBuffer = readFileSync(imagePath);
|
|
747
|
+
const fileName = imagePath.split("/").pop() || `phone-${i + 1}.png`;
|
|
748
|
+
await this.uploadImageWithOptionalTimeout(session, language, "phoneScreenshots", imageBuffer, imageUploadTimeoutMs);
|
|
749
|
+
console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
|
|
750
|
+
result.uploaded.phoneScreenshots++;
|
|
751
|
+
}
|
|
752
|
+
for (let i = 0; i < sevenInchScreenshots.length; i++) {
|
|
753
|
+
const imagePath = sevenInchScreenshots[i];
|
|
754
|
+
if (!existsSync(imagePath)) {
|
|
755
|
+
console.error(`[GooglePlayClient] Warning: 7-inch screenshot not found: ${imagePath}`);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const imageBuffer = readFileSync(imagePath);
|
|
759
|
+
const fileName = imagePath.split("/").pop() || `tablet7-${i + 1}.png`;
|
|
760
|
+
await this.uploadImageWithOptionalTimeout(session, language, "sevenInchScreenshots", imageBuffer, imageUploadTimeoutMs);
|
|
761
|
+
console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
|
|
762
|
+
result.uploaded.sevenInchScreenshots++;
|
|
763
|
+
}
|
|
764
|
+
for (let i = 0; i < tenInchScreenshots.length; i++) {
|
|
765
|
+
const imagePath = tenInchScreenshots[i];
|
|
766
|
+
if (!existsSync(imagePath)) {
|
|
767
|
+
console.error(`[GooglePlayClient] Warning: 10-inch screenshot not found: ${imagePath}`);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const imageBuffer = readFileSync(imagePath);
|
|
771
|
+
const fileName = imagePath.split("/").pop() || `tablet10-${i + 1}.png`;
|
|
772
|
+
await this.uploadImageWithOptionalTimeout(session, language, "tenInchScreenshots", imageBuffer, imageUploadTimeoutMs);
|
|
773
|
+
console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
|
|
774
|
+
result.uploaded.tenInchScreenshots++;
|
|
775
|
+
}
|
|
776
|
+
if (featureGraphic && existsSync(featureGraphic)) {
|
|
777
|
+
const imageBuffer = readFileSync(featureGraphic);
|
|
778
|
+
await this.uploadImageWithOptionalTimeout(session, language, "featureGraphic", imageBuffer, imageUploadTimeoutMs);
|
|
779
|
+
console.error(`[GooglePlayClient] ✅ Uploaded feature-graphic.png`);
|
|
780
|
+
result.uploaded.featureGraphic = true;
|
|
781
|
+
}
|
|
782
|
+
return result;
|
|
750
783
|
}
|
|
751
784
|
async uploadImageWithOptionalTimeout(session, language, imageType, imageBuffer, timeoutMs) {
|
|
752
785
|
await this.androidPublisher.edits.images.upload({
|
|
@@ -5,11 +5,14 @@ import { AppResolutionService } from "../../core/services/app-resolution-service
|
|
|
5
5
|
import { AppStoreService } from "../../core/services/app-store-service.js";
|
|
6
6
|
import { GooglePlayService } from "../../core/services/google-play-service.js";
|
|
7
7
|
import { formatPushResult } from "../../core/helpers/formatters.js";
|
|
8
|
+
const DEFAULT_IMAGE_UPLOAD_TIMEOUT_MS = 10 * 60 * 1000;
|
|
8
9
|
const appResolutionService = new AppResolutionService();
|
|
9
10
|
const appStoreService = new AppStoreService();
|
|
10
11
|
const googlePlayService = new GooglePlayService();
|
|
11
12
|
export async function handleAsoPush(options) {
|
|
12
|
-
const { store = "both", uploadImages = false, locales,
|
|
13
|
+
const { store = "both", uploadImages = false, locales, dryRun = false, } = options;
|
|
14
|
+
const imageUploadTimeoutMs = options.imageUploadTimeoutMs ??
|
|
15
|
+
(uploadImages ? DEFAULT_IMAGE_UPLOAD_TIMEOUT_MS : undefined);
|
|
13
16
|
const resolved = appResolutionService.resolve({
|
|
14
17
|
slug: options.app,
|
|
15
18
|
packageName: options.packageName,
|