t1k:marketing:ua:app-metadata
| Field | Value |
|---|---|
| Module | ua |
| Version | 1.8.4 |
| Effort | low |
| Tools | — |
How to invoke
Section titled “How to invoke”/t1k:marketing:ua:app-metadataApp Store Metadata Lookup
Section titled “App Store Metadata Lookup”Given an app identifier, return { name, iconUrl, storeUrl, platform }.
Origin: adapted from ClaudeAssistant store-review (app-lookup.service.ts).
First consumer: marketing-hub ROAS per-app table.
When to Use
Section titled “When to Use”- Display an app icon / logo next to ROAS or revenue rows
- Resolve a package name or bundle ID to a human-readable app name
- Populate store URL + platform for a new app record
- Bulk back-fill icon URLs for existing rows with
nullicons
Platform Detection
Section titled “Platform Detection”dots present → reverse-DNS → try Android first, iOS bundleId fallbackpurely numeric → iOS App Store ID (id=)Android — google-play-scraper
Section titled “Android — google-play-scraper”npm install google-play-scraper@^10.1.2import gplay from 'google-play-scraper';
async function lookupAndroid(packageName: string) { const app = await gplay.app({ appId: packageName }); return { platform: 'android' as const, name: app.title, iconUrl: app.icon, storeUrl: app.url };}Throws Error('app not found') when package is absent — catch and treat as notFound.
iOS — iTunes Lookup API (no auth)
Section titled “iOS — iTunes Lookup API (no auth)”async function lookupIos(idOrBundle: string, field: 'id' | 'bundleId') { const url = `https://itunes.apple.com/lookup?${field}=${encodeURIComponent(idOrBundle)}`; const json = await fetch(url).then(r => r.json()) as { resultCount: number; results: any[] }; if (json.resultCount === 0) return null; const r = json.results[0]; return { platform: 'ios' as const, name: r.trackName, iconUrl: r.artworkUrl512 ?? r.artworkUrl100 ?? '', storeUrl: r.trackViewUrl };}resultCount === 0 is the canonical “not found” — never throw on it.
Combined Resolver
Section titled “Combined Resolver”export async function resolveAppMetadata(id: string) { if (/^\d+$/.test(id)) return lookupIos(id, 'id'); try { return await lookupAndroid(id); } catch { return lookupIos(id, 'bundleId'); }}Caching
Section titled “Caching”Store results in app_metadata_cache (columns: package_name, name,
icon_url, store_url, platform, resolved_at, not_found).
Refresh TTL ≈ 30 days. Mark not_found = TRUE to avoid re-hitting the API.
Batch: 500 ms inter-lookup delay. See references/implementation.md for
schema, batch wrapper, and artworkUrl size priority.
Isolation Rule (MANDATORY)
Section titled “Isolation Rule (MANDATORY)”These are external vendor calls — run only in a scheduler / cron / job
context. Dashboard / read endpoints query app_metadata_cache only; a cache
miss returns empty (never triggers a live fetch). See project CLAUDE.md
§ “Live-API Isolation”.
Gotchas
Section titled “Gotchas”google-play-scraperthrows on a missing package — catch per-item, never abort the batch.resultCount === 0from iTunes ≠ HTTP error — it is a successful “not found”.- Purely numeric identifiers are always iOS; passing them to
gplay.app()throws. artworkUrl512may be absent for older apps; fall back toartworkUrl100.not_foundcache flag prevents infinite re-fetching of removed/region-restricted apps.
References
Section titled “References”references/implementation.md— DB schema, batch wrapper, artworkUrl sizes- google-play-scraper: https://www.npmjs.com/package/google-play-scraper
- iTunes Search API: https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/index.html