Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/bubble-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bubblelab/bubble-core",
"version": "0.1.237",
"version": "0.1.238",
"type": "module",
"license": "Apache-2.0",
"main": "./dist/index.js",
Expand Down
8 changes: 8 additions & 0 deletions packages/bubble-core/src/bubble-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export class BubbleFactory {
'tiktok-tool',
'twitter-tool',
'google-maps-tool',
'app-rankings-tool',
'youtube-tool',
'github',
'eleven-labs',
Expand Down Expand Up @@ -329,6 +330,9 @@ export class BubbleFactory {
const { GoogleMapsTool } = await import(
'./bubbles/tool-bubble/google-maps-tool.js'
);
const { AppRankingsTool } = await import(
'./bubbles/tool-bubble/app-rankings-tool.js'
);
const { SlackFormatterAgentBubble } = await import(
'./bubbles/workflow-bubble/slack-formatter-agent.js'
);
Expand Down Expand Up @@ -536,6 +540,10 @@ export class BubbleFactory {
'google-maps-tool',
GoogleMapsTool as BubbleClassWithMetadata
);
this.register(
'app-rankings-tool',
AppRankingsTool as BubbleClassWithMetadata
);
this.register('youtube-tool', YouTubeTool as BubbleClassWithMetadata);
this.register('web-crawl-tool', WebCrawlTool as BubbleClassWithMetadata);
this.register('eleven-labs', ElevenLabsBubble as BubbleClassWithMetadata);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { z } from 'zod';

const APPLE_COUNTRIES = [
'us',
'gb',
'ca',
'au',
'de',
'fr',
'jp',
'kr',
'cn',
'in',
'br',
'mx',
'es',
'it',
'nl',
'se',
'no',
'dk',
'fi',
'pl',
'ru',
'tr',
'sa',
'ae',
'za',
'eg',
'ng',
'ke',
'id',
'th',
'vn',
'ph',
'my',
'sg',
'tw',
'hk',
'nz',
'at',
'be',
'ch',
'cz',
'hu',
'ro',
'bg',
'hr',
'sk',
'si',
'pt',
'gr',
'ie',
'il',
'ar',
'cl',
'co',
'pe',
'uy',
'ec',
'cr',
'pa',
'do',
] as const;

const APPLE_CATEGORIES = [
'overall',
'6000',
'6001',
'6002',
'6003',
'6004',
'6005',
'6006',
'6007',
'6008',
'6009',
'6010',
'6011',
'6012',
'6013',
'6014',
'6015',
'6016',
'6017',
'6018',
'6020',
'6023',
'6024',
'6025',
'6026',
'6027',
] as const;

const APPLE_CHART_TYPES = [
'topfreeapplications',
'toppaidapplications',
'topgrossingapplications',
] as const;

const GOOGLE_CATEGORIES = [
'APPLICATION',
'GAME',
'ART_AND_DESIGN',
'AUTO_AND_VEHICLES',
'BEAUTY',
'BOOKS_AND_REFERENCE',
'BUSINESS',
'COMICS',
'COMMUNICATION',
'DATING',
'EDUCATION',
'ENTERTAINMENT',
'EVENTS',
'FINANCE',
'FOOD_AND_DRINK',
'HEALTH_AND_FITNESS',
'HOUSE_AND_HOME',
'LIBRARIES_AND_DEMO',
'LIFESTYLE',
'MAPS_AND_NAVIGATION',
'MEDICAL',
'MUSIC_AND_AUDIO',
'NEWS_AND_MAGAZINES',
'PARENTING',
'PERSONALIZATION',
'PHOTOGRAPHY',
'PRODUCTIVITY',
'SHOPPING',
'SOCIAL',
'SPORTS',
'TOOLS',
'TRAVEL_AND_LOCAL',
'VIDEO_PLAYERS',
'WEATHER',
] as const;

const GOOGLE_CHART_TYPES = [
'topselling_free',
'topselling_paid',
'topgrossing',
] as const;

export const AppRankingsScraperInputSchema = z.object({
stores: z
.array(z.enum(['apple', 'google']))
.min(1)
.default(['apple', 'google'])
.describe('App stores to scrape. Both stores supported in a single run.'),

appleCountries: z
.array(z.enum(APPLE_COUNTRIES))
.default(['us'])
.optional()
.describe(
'ISO country codes for Apple App Store (e.g., ["us", "jp", "de"])'
),

appleCategories: z
.array(z.enum(APPLE_CATEGORIES))
.default(['overall'])
.optional()
.describe(
'Apple category IDs. "overall" = all apps. Numeric IDs: 6014=Games, 6015=Finance, 6013=Health & Fitness, etc.'
),

appleChartTypes: z
.array(z.enum(APPLE_CHART_TYPES))
.default(['topfreeapplications'])
.optional()
.describe(
'Apple chart types: topfreeapplications, toppaidapplications, topgrossingapplications'
),

googleCountries: z
.array(z.enum(APPLE_COUNTRIES))
.default(['us'])
.optional()
Comment on lines +175 to +178
.describe('ISO country codes for Google Play Store'),

googleCategories: z
.array(z.enum(GOOGLE_CATEGORIES))
.default(['APPLICATION'])
.optional()
.describe(
'Google Play category IDs. "APPLICATION" = overall. Examples: GAME, FINANCE, HEALTH_AND_FITNESS'
),

googleChartTypes: z
.array(z.enum(GOOGLE_CHART_TYPES))
.default(['topselling_free'])
.optional()
.describe(
'Google chart types: topselling_free, topselling_paid, topgrossing'
),

limit: z
.number()
.min(1)
.max(100)
.default(100)
.optional()
.describe('Number of apps per chart (1-100, default: 100)'),
});

export const AppRankingsItemSchema = z.object({
store: z
.enum(['apple', 'google'])
.describe('Which app store this ranking is from'),

appId: z.string().describe('Unique app identifier (bundle ID or store ID)'),

rank: z.number().describe('Chart position (1-indexed)'),

iconUrl: z.string().describe('App icon image URL'),

appName: z.string().describe('App display name'),

appUrl: z.string().describe('Direct link to store listing'),

rating: z
.union([z.number(), z.string()])
.describe('Average user rating (number) or "Not enough reviews yet"'),

ratingCount: z
.union([z.number(), z.string()])
.describe('Total ratings count (number) or "Rating count not available"'),

price: z.string().describe('Price display string ("Free" for free apps)'),

developer: z.string().describe('Developer or publisher name'),

category: z
.string()
.describe('Human-readable category name (e.g., "Overall", "Games")'),

chartType: z
.string()
.describe('Normalized chart type (e.g., "top-free", "top-paid")'),

genres: z.string().describe('Comma-separated genre tags'),

country: z.string().describe('ISO 3166-1 alpha-2 country code'),

releaseDate: z
.string()
.describe('Original release date or "Release date not available"'),

scrapedAt: z
.string()
.describe('UTC timestamp (ISO 8601) of when data was scraped'),
});

export type AppRankingsScraperInput = z.output<
typeof AppRankingsScraperInputSchema
>;
export type AppRankingsItem = z.output<typeof AppRankingsItemSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ import {
GoogleMapsScraperInputSchema,
GoogleMapsPlaceSchema,
} from './actors/google-maps-scraper.js';
import {
AppRankingsScraperInputSchema,
AppRankingsItemSchema,
} from './actors/app-rankings-scraper.js';

// ============================================================================
// ACTOR REGISTRY
Expand Down Expand Up @@ -129,4 +133,13 @@ export const APIFY_ACTOR_SCHEMAS = {
documentation: 'https://apify.com/compass/crawler-google-places',
category: 'maps',
},
'slothtechlabs/ios-android-app-rankings-scraper': {
input: AppRankingsScraperInputSchema,
output: AppRankingsItemSchema,
description:
'Scrape Apple App Store and Google Play top chart rankings (Top Free, Top Paid, Top Grossing) across 60+ countries and 50+ categories',
documentation:
'https://apify.com/slothtechlabs/ios-android-app-rankings-scraper',
category: 'apps',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export class ApifyBubble<T extends string = string> extends ServiceBubble<
This is a generic service bubble that can execute any Apify actor with any input.
Actor-specific logic and data transformation should be handled by Tool Bubbles.

Integrated Actors, use them through instagram-tool, reddit-tool, linkedin-tool, youtube-tool, tiktok-tool, twitter-tool, google-maps-tool, etc, not directly:
Integrated Actors, use them through instagram-tool, reddit-tool, linkedin-tool, youtube-tool, tiktok-tool, twitter-tool, google-maps-tool, app-rankings-tool, etc, not directly:
- apify/instagram-scraper - Instagram posts, profiles, hashtags
- apify/instagram-hashtag-scraper - Instagram hashtag posts
- harvestapi/linkedin-profile-scraper - LinkedIn profile details (name, experience, education, skills)
Expand All @@ -200,6 +200,7 @@ export class ApifyBubble<T extends string = string> extends ServiceBubble<
- clockworks/tiktok-scraper - TikTok profiles, videos, hashtags
- apidojo/tweet-scraper - Twitter/X profiles, tweets, search results
- compass/crawler-google-places - Google Maps business listings and reviews
- slothtechlabs/ios-android-app-rankings-scraper - Apple & Google Play top chart rankings
- IMPORTANT: For other actors, use discovery mode to find the actor and its page, then use the web scrape tool to scrape the input schema page to get the input/output schema details.

Discovery Mode:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ export {
YouTubeTranscriptItemSchema,
YouTubeTranscriptResultSchema,
} from './actors/youtube-transcript-scraper.js';

// App Rankings schemas
export {
AppRankingsScraperInputSchema,
AppRankingsItemSchema,
} from './actors/app-rankings-scraper.js';
Loading
Loading