play images: tablet, TV and Wear sets are optional; play.tablets switch (default: tablet renders exist); a missing optional set never deletes Play's images without --prune

A phone-only project aborted with 'no sevenInchScreenshots'. Play requires the 512 icon, the 1024x500 feature graphic and at least 2 screenshots (Play Console Help answer 9866151). play images uploads what exists and prints an info line for missing optional sets; play.tablets true makes missing tablet sets an error in shots render/check/gallery, validate and play images. Pitfall 19.
This commit is contained in:
CRTkafa 2026-10-12 08:21:03 +08:00
parent 18fde37b9b
commit a9261a8052
18 changed files with 364 additions and 83 deletions

View file

@ -509,6 +509,11 @@ export interface ShipConfig {
name?: string;
/** Free (true) or paid (false) on Play. A free app can never become paid later; storebound flags it for the owner. */
free?: boolean;
/** Play 7" and 10" tablet screenshots (renders tab7/ and tab10/). Optional on Play (required: icon, feature graphic,
* at least 2 phone screenshots), but without them the app is not eligible for tablet featuring. Default: true when
* tablet renders exist under the renders folder, else false. false = `shots render` / `shots check`, `validate` and
* `play images` never ask for them; true = missing tablet sets are errors. */
tablets?: boolean;
/** Promo video: a YouTube URL (public or unlisted, ads off, embeddable). Play takes no video upload. Sent by `play listing`. */
video?: string;
/** Cloud Storage bucket of the Play Console report exports, e.g. "pubsite_prod_rev_<number>" (Play Console >

View file

@ -98,6 +98,8 @@ export interface PlaySnapshot {
expectedProducts: string[];
/** Local renders (Play sizes only), checked before upload. */
renders?: RendersCheck;
/** Resolved play.tablets: true = 7" and 10" tablet screenshots are required too. */
tablets?: boolean;
/** Play subscriptions and their base plans (validate reads them when the config has subscriptions). */
subscriptions?: { productId: string; basePlans: { basePlanId: string; state?: string }[] }[];
expectedSubscriptions?: { id: string; basePlanId: string }[];
@ -186,7 +188,13 @@ export function appleRules(s: AppleSnapshot): Finding[] {
return sortFindings(f);
}
// Play Console Help, "Add preview assets to showcase your app" (answer 9866151, checked October 2026): an app icon and a
// feature graphic are required, and at least 2 screenshots; 7" / 10" tablet screenshots are optional (play.tablets).
export const PLAY_REQUIRED_IMAGES: Record<string, number> = { icon: 1, featureGraphic: 1, phoneScreenshots: 2 };
/** Play tablet image types, required only with play.tablets (resolved: explicit, or tablet renders exist). */
export const PLAY_TABLET_IMAGES: Record<string, number> = { sevenInchScreenshots: 1, tenInchScreenshots: 1 };
/** Image types validate requires on Play for a project. */
export const playRequiredImages = (tablets: boolean | undefined): Record<string, number> => (tablets ? { ...PLAY_REQUIRED_IMAGES, ...PLAY_TABLET_IMAGES } : PLAY_REQUIRED_IMAGES);
/** app-ads.txt lines compared without spaces and case (the format is case-insensitive except the ids, which we keep). */
export const normAdsLine = (l: string) => l.split('#')[0]!.split(',').map(p => p.trim().toLowerCase()).filter(Boolean).join(',');
@ -200,7 +208,8 @@ export function playRules(s: PlaySnapshot): Finding[] {
for (const l of s.listings) {
const miss = (['title', 'shortDescription', 'fullDescription'] as const).filter(k => !l[k]);
if (miss.length) add('blocker', 'listing', `${l.language}: missing ${miss.join(', ')}`, 'storebound play listing');
for (const [type, n] of Object.entries(PLAY_REQUIRED_IMAGES)) if ((l.images[type] ?? 0) < n) add('blocker', 'images', `${l.language}: ${type} ${l.images[type] ?? 0}/${n}`, 'storebound play images');
for (const [type, n] of Object.entries(playRequiredImages(s.tablets))) if ((l.images[type] ?? 0) < n)
add('blocker', 'images', `${l.language}: ${type} ${l.images[type] ?? 0}/${n}${PLAY_TABLET_IMAGES[type] ? ' (play.tablets is on)' : ''}`, 'storebound play images');
}
if (s.listings.length && !f.some(x => x.code === 'listing' || x.code === 'images')) add('ok', 'listing', `${s.listings.length} listings with text and graphics`);