Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
121 lines
3.6 KiB
JavaScript
121 lines
3.6 KiB
JavaScript
// Script to query Apple iTunes Search API for ASO & Competition Analysis
|
|
import https from 'https';
|
|
|
|
const KEYWORDS_DE = [
|
|
'receipt scanner',
|
|
'beleg scanner',
|
|
'belege digitalisieren',
|
|
'rechnung scanner',
|
|
'kassenbon scanner',
|
|
'quittung scanner',
|
|
'spesen app',
|
|
'buchhaltung scanner',
|
|
'datev scanner',
|
|
'receipt to excel',
|
|
'expense tracker',
|
|
'ausgaben tracker',
|
|
'belegmanager',
|
|
'rechnungsprogramm',
|
|
'fahrtenbuch und belege',
|
|
'steuer belege',
|
|
'ocr scanner excel'
|
|
];
|
|
|
|
const KEYWORDS_US = [
|
|
'receipt scanner',
|
|
'receipt to excel',
|
|
'receipt scanner to excel',
|
|
'expense tracker',
|
|
'receipt keeper',
|
|
'invoice scanner',
|
|
'receipts and expenses',
|
|
'ocr receipt scanner',
|
|
'bookkeeping scanner',
|
|
'mileage and receipts',
|
|
'tax receipt organizer',
|
|
'smart receipt'
|
|
];
|
|
|
|
function fetchAppleSearch(term, country = 'de', limit = 25) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = `https://itunes.apple.com/search?term=${encodeURIComponent(term)}&country=${country}&entity=software&limit=${limit}`;
|
|
https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
try {
|
|
const json = JSON.parse(data);
|
|
resolve(json);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
}).on('error', reject);
|
|
});
|
|
}
|
|
|
|
async function analyzeKeywords(keywords, country) {
|
|
console.log(`\n======================================================`);
|
|
console.log(`🔍 ANALYSING APPLE APP STORE SEARCH DATA [Country: ${country.toUpperCase()}]`);
|
|
console.log(`======================================================\n`);
|
|
|
|
const results = [];
|
|
|
|
for (const kw of keywords) {
|
|
try {
|
|
const data = await fetchAppleSearch(kw, country, 25);
|
|
const totalResults = data.resultCount;
|
|
const apps = data.results || [];
|
|
|
|
// Metrics calculation
|
|
const top5 = apps.slice(0, 5);
|
|
const top5Names = top5.map(a => a.trackName);
|
|
const avgRating = top5.reduce((acc, a) => acc + (a.averageUserRating || 0), 0) / (top5.length || 1);
|
|
const totalTop5Ratings = top5.reduce((acc, a) => acc + (a.userRatingCount || 0), 0);
|
|
const avgTop5RatingCount = Math.round(totalTop5Ratings / (top5.length || 1));
|
|
|
|
// Check title keyword match density in top 10
|
|
const titleMatches = apps.slice(0, 10).filter(a =>
|
|
(a.trackName || '').toLowerCase().includes(kw.toLowerCase()) ||
|
|
(a.description || '').toLowerCase().includes(kw.toLowerCase())
|
|
).length;
|
|
|
|
// Price distribution
|
|
const freeCount = top5.filter(a => a.price === 0).length;
|
|
|
|
results.push({
|
|
keyword: kw,
|
|
resultCount: totalResults,
|
|
top1: apps[0] ? `${apps[0].trackName} (${apps[0].userRatingCount || 0} reviews, ★${apps[0].averageUserRating?.toFixed(1) || '0'})` : 'None',
|
|
top5AvgRating: avgRating.toFixed(2),
|
|
avgTop5Reviews: avgTop5RatingCount,
|
|
titleMatchInTop10: titleMatches,
|
|
topCompetitors: top5.map(a => ({
|
|
name: a.trackName,
|
|
seller: a.sellerName,
|
|
reviews: a.userRatingCount || 0,
|
|
rating: a.averageUserRating || 0,
|
|
price: a.price,
|
|
genres: a.genres
|
|
}))
|
|
});
|
|
|
|
// Avoid hitting rate limits
|
|
await new Promise(r => setTimeout(r, 200));
|
|
} catch (err) {
|
|
console.error(`Error fetching "${kw}":`, err.message);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
async function run() {
|
|
const deResults = await analyzeKeywords(KEYWORDS_DE, 'de');
|
|
const usResults = await analyzeKeywords(KEYWORDS_US, 'us');
|
|
|
|
console.log(JSON.stringify({ de: deResults, us: usResults }, null, 2));
|
|
}
|
|
|
|
run();
|