iPhone Duo: limit from Apple's reference data instead of a cut to 10, --duo-only; shared asset library helpers in their own module
This commit is contained in:
parent
5cc3da486d
commit
108b362cd9
7 changed files with 102 additions and 75 deletions
57
cli/apple/asset-library.ts
Normal file
57
cli/apple/asset-library.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// App Asset Library states and upsert rules shared by creative assets and iPhone Duo screenshots. No command flags here.
|
||||
import * as rt from '../lib/rt';
|
||||
import type { J } from './asc';
|
||||
|
||||
export const REF = (sum: string): string => `storebound ${sum}`;
|
||||
export const sumOf = (a: J): string | undefined => /^storebound ([0-9a-f]{32})$/.exec(String(a?.attributes?.referenceName ?? ''))?.[1];
|
||||
|
||||
/** Asset states (AppAssetLibraryAssetState, API 4.5.1) by what storebound may do with them. */
|
||||
export const ASSET_STATES = {
|
||||
/** still uploading / processing: wait (bounded), then select */
|
||||
processing: ['AWAITING_UPLOAD', 'UPLOAD_COMPLETE'],
|
||||
/** processed and usable in a slot; not reviewed yet: goes into a review submission */
|
||||
unreviewed: ['COMPLETE', 'PREPARE_FOR_SUBMISSION'],
|
||||
/** in a submission or reviewed: usable in a slot, nothing to add */
|
||||
reviewing: ['READY_FOR_REVIEW', 'WAITING_FOR_REVIEW', 'IN_REVIEW', 'ACCEPTED', 'APPROVED'],
|
||||
} as const;
|
||||
export type AssetClass = 'processing' | 'unreviewed' | 'reviewing' | 'failed' | 'rejected' | 'archived' | 'unknown';
|
||||
|
||||
/** What a library asset's state means for storebound. Pure. */
|
||||
export function classify(state: string | undefined): AssetClass {
|
||||
const s = String(state ?? '');
|
||||
for (const [k, v] of Object.entries(ASSET_STATES)) if ((v as readonly string[]).includes(s)) return k as AssetClass;
|
||||
if (s === 'FAILED') return 'failed';
|
||||
if (s === 'REJECTED') return 'rejected';
|
||||
if (s === 'ARCHIVED') return 'archived';
|
||||
return s ? 'unknown' : 'processing';
|
||||
}
|
||||
|
||||
/** Selectable for a slot: processed and not failed / rejected / archived (Prepare for Submission is enough). */
|
||||
export const selectable = (c: AssetClass): boolean => c === 'unreviewed' || c === 'reviewing';
|
||||
|
||||
/** Stop re-uploading a file whose earlier uploads failed processing this many times. */
|
||||
export const MAX_FAILED = 2;
|
||||
|
||||
/** The library asset to use for one MD5: a live one first (newest state wins: reviewing > unreviewed > processing),
|
||||
* else an archived one (unarchive it), else none; plus why not to upload (a rejected file, repeated failures). Pure. */
|
||||
export function pickAsset(assets: J[], sum: string): { asset?: J; unarchive?: boolean; refuse?: string } {
|
||||
const mine = assets.filter(x => sumOf(x) === sum);
|
||||
const rank: Record<AssetClass, number> = { reviewing: 0, unreviewed: 1, processing: 2, unknown: 3, archived: 4, rejected: 5, failed: 6 };
|
||||
const by = (c: AssetClass) => mine.filter(x => classify(x.attributes?.state) === c);
|
||||
const live = mine.filter(x => rank[classify(x.attributes?.state)] <= 3).sort((a, b) => rank[classify(a.attributes?.state)] - rank[classify(b.attributes?.state)]);
|
||||
if (live[0]) return { asset: live[0] };
|
||||
if (by('archived')[0]) return { asset: by('archived')[0], unarchive: true };
|
||||
const rej = by('rejected')[0];
|
||||
if (rej) return { refuse: `rejected by App Review${reason(rej)}: uploading the same file again changes nothing; change the file` };
|
||||
if (by('failed').length >= MAX_FAILED) return { refuse: `${by('failed').length} earlier uploads of this file failed processing${reason(by('failed')[0]!)}: check the file (format, size) before trying again` };
|
||||
return {};
|
||||
}
|
||||
|
||||
export const reason = (a: J): string => {
|
||||
const d = ((a.attributes?.stateDetails ?? []) as J[]).map(x => x?.code ?? x?.message ?? x?.description).filter(Boolean);
|
||||
return d.length ? ` (${d.join(', ')})` : '';
|
||||
};
|
||||
|
||||
/** Bounded wait for processing (every wait has a timeout and a heartbeat); tests set waitMs 0 / timeoutMs 0. */
|
||||
export const processingTiming = { pollMs: 3000, timeoutMs: 120_000, heartbeatMs: 20_000, sleep: (ms: number): Promise<void> => (ms > 0 ? rt.sleep(ms) : Promise.resolve()) };
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue