PlayHound is an unofficial, typed Google Play metadata scraper for Rust. It provides:
- an async-first Rust library;
- an optional native blocking client;
- a command-line interface;
- application details, search, collections, reviews, and suggestions;
- HTTP, HTTPS, SOCKS5, and SOCKS5h proxy support;
- localized requests, throttling, bounded retries, and response-size limits;
- Serde-compatible public models and structured errors.
Google Play does not provide a supported public API for this storefront data. Its HTML and internal RPC response formats can change without notice. Use conservative request rates, cache responses where appropriate, and comply with all applicable terms and laws.
- Capabilities
- Installation
- Feature flags
- Quick start
- Async library API
- Blocking library API
- Client configuration
- Localization
- Proxies
- Retries and rate limiting
- Result models
- Errors
- CLI installation
- CLI command reference
- CLI configuration and environment variables
- Operational notes
- Testing
- MSRV and platforms
- License
| Operation | Rust API | CLI | Result |
|---|---|---|---|
| Application details | Client::app |
playhound app |
AppDetails |
| Search | Client::search |
playhound search |
Vec<AppOverview> |
| Collections | Client::list |
playhound list |
Vec<AppOverview> |
| Reviews | Client::reviews |
playhound reviews |
Page<Review> |
| Suggestions | Client::suggestions |
playhound suggest |
Vec<String> |
Application details include localized descriptions, exact price micros, rating statistics, install ranges, developer contact information, media URLs, content rating, release information, version information, and release notes when Google supplies them.
Search supports free/paid filtering and follows a continuation token when the storefront provides one. The current storefront normally returns at most about 30 search results and may provide no continuation token, so a requested limit is an upper bound rather than a guaranteed result count.
Collections support top-free, top-paid, and top-grossing lists, all modeled categories, family age filters, and forward-compatible custom collection and category values.
The default feature builds the CLI. Disable it when PlayHound is used only as a library:
[dependencies]
playhound = { version = "0.1", default-features = false }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }[dependencies]
playhound = { version = "0.1", default-features = false, features = ["blocking"] }[dependencies]
playhound = "0.1"During local development, a path dependency can be used:
[dependencies]
playhound = { path = "../google-play-scraper", default-features = false }| Feature | Default | Purpose |
|---|---|---|
cli |
Yes | Builds the playhound binary and its CLI/runtime dependencies. |
blocking |
No | Enables playhound::blocking::Client using Reqwest's native blocking client. |
The asynchronous playhound::Client is always available. Enabling
blocking adds the synchronous API; it does not replace the async client.
use playhound::{Client, SearchQuery};
#[tokio::main]
async fn main() -> playhound::Result<()> {
let client = Client::new()?;
let apps = client
.search(SearchQuery::new("example query").limit(10))
.await?;
for app in apps {
println!("{} ({})", app.title, app.app_id);
}
Ok(())
}All network operations return playhound::Result<T>. Inputs are validated
before transmission.
Client::app accepts an application ID directly:
use playhound::Client;
# async fn example() -> playhound::Result<()> {
let client = Client::new()?;
let app = client.app("com.example.app").await?;
println!("{}", app.overview.title);
println!("{:?}", app.version);
println!("{:?}", app.description);
# Ok(())
# }Use AppRequest to override the locale for one request:
use playhound::{AppRequest, Client, Locale};
# async fn example() -> playhound::Result<()> {
let client = Client::new()?;
let request = AppRequest::new("com.example.app")
.locale(Locale::new("de", "de")?);
let app = client.app(request).await?;
# Ok(())
# }use playhound::{Client, PriceFilter, SearchQuery};
# async fn example() -> playhound::Result<()> {
let client = Client::new()?;
let apps = client
.search(
SearchQuery::new("example query")
.limit(20)
.price(PriceFilter::Free),
)
.await?;
# Ok(())
# }PriceFilter values are:
PriceFilter::All;PriceFilter::Free;PriceFilter::Paid.
The default search limit is 20. Limits must be nonzero and fit in a signed 32-bit wire value. Google may return fewer results than requested.
use playhound::{AgeRange, Category, Client, Collection, ListQuery};
# async fn example() -> playhound::Result<()> {
let client = Client::new()?;
let apps = client
.list(
ListQuery::new(Collection::TopFree, Category::Education)
.age(AgeRange::SixToEight)
.limit(25),
)
.await?;
# Ok(())
# }ListQuery::default() requests the top-free APPLICATION collection with
a limit of 50.
Supported collection values:
| Rust value | Wire/CLI value |
|---|---|
Collection::TopFree |
topselling_free |
Collection::TopPaid |
topselling_paid |
Collection::TopGrossing |
topgrossing |
Supported age values:
| Rust value | CLI values |
|---|---|
AgeRange::FiveAndUnder |
five-and-under, 5 |
AgeRange::SixToEight |
six-to-eight, 6-8 |
AgeRange::NineAndUp |
nine-and-up, 9+ |
The modeled category wire values are:
GAME
FAMILY
APPLICATION
ANDROID_WEAR
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
WATCH_FACE
WEATHER
GAME_ACTION
GAME_ADVENTURE
GAME_ARCADE
GAME_BOARD
GAME_CARD
GAME_CASINO
GAME_CASUAL
GAME_EDUCATIONAL
GAME_MUSIC
GAME_PUZZLE
GAME_RACING
GAME_ROLE_PLAYING
GAME_SIMULATION
GAME_SPORTS
GAME_STRATEGY
GAME_TRIVIA
GAME_WORD
Collection and Category are forward-compatible. Unknown printable
values can be parsed or created explicitly:
use playhound::{Category, Collection};
let collection: Collection = "future_collection".parse()?;
let category = Category::custom("FUTURE_CATEGORY")?;
# Ok::<(), playhound::Error>(())use playhound::{Client, ReviewQuery, ReviewSort};
# async fn example() -> playhound::Result<()> {
let client = Client::new()?;
let page = client
.reviews(
ReviewQuery::new("com.example.app")
.sort(ReviewSort::Newest)
.page_size(50),
)
.await?;
for review in &page.items {
println!("[{}/5] {}", review.score, review.user_name);
}
# Ok(())
# }ReviewSort values are Newest, Rating, and Helpfulness. The
default is newest-first with a page size of 100.
Review pagination tokens are opaque. Pass them back unchanged:
use playhound::{Client, ReviewQuery};
# async fn example() -> playhound::Result<()> {
let client = Client::new()?;
let first = client
.reviews(ReviewQuery::new("com.example.app").page_size(50))
.await?;
if let Some(token) = first.next_page_token {
let second = client
.reviews(
ReviewQuery::new("com.example.app")
.page_size(50)
.page_token(token),
)
.await?;
println!("second page: {} reviews", second.items.len());
}
# Ok(())
# }Do not log or interpret PageToken values. Its Debug implementation
reveals only the token length; expose() is available when the raw value is
needed for persistence or a subsequent request.
use playhound::{Client, SuggestionQuery};
# async fn example() -> playhound::Result<()> {
let client = Client::new()?;
let suggestions = client
.suggestions(SuggestionQuery::new("example"))
.await?;
# Ok(())
# }A string can be passed directly when no per-request locale is needed:
# async fn example() -> playhound::Result<()> {
let client = playhound::Client::new()?;
let suggestions = client.suggestions("example").await?;
# Ok(())
# }Enable the blocking feature and use playhound::blocking::Client. The
operation and request types are shared with the async client:
use playhound::{
PriceFilter, ReviewQuery, SearchQuery,
blocking::Client,
};
fn main() -> playhound::Result<()> {
let client = Client::new()?;
let apps = client.search(
SearchQuery::new("example query")
.limit(10)
.price(PriceFilter::All),
)?;
let reviews = client.reviews(
ReviewQuery::new("com.example.app").page_size(25),
)?;
println!("{} apps, {} reviews", apps.len(), reviews.items.len());
Ok(())
}The blocking implementation uses Reqwest's native blocking client and does not create an async runtime internally. Do not call it directly from an async task; use the async client or move blocking work to an appropriate blocking thread.
Both clients provide the same builder options:
use std::{num::NonZeroU32, time::Duration};
use playhound::{Client, Locale, Proxy, RetryPolicy};
let client = Client::builder()
.default_locale(Locale::new("en", "us")?)
.proxy(Proxy::all("socks5h://127.0.0.1:1080")?)
.use_system_proxy(false)
.request_timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.max_response_bytes(32 * 1024 * 1024)
.requests_per_second(NonZeroU32::new(2).expect("nonzero"))
.retry_policy(
RetryPolicy::exponential(2)
.base_delay(Duration::from_millis(250))
.max_delay(Duration::from_secs(3))
.honor_retry_after(true),
)
.build()?;
# Ok::<(), playhound::Error>(())Default configuration:
| Setting | Default |
|---|---|
| Locale | en / us |
| Environment/system proxy discovery | Enabled |
| Request timeout | 30 seconds |
| Connection timeout | 10 seconds |
| Maximum response body | 32 MiB |
| Client-side rate limit | Disabled |
| Retries | Disabled |
| TLS certificate verification | Enabled |
Timeouts and the maximum response size must be nonzero. A retry policy's maximum delay must not be shorter than its base delay.
danger_accept_invalid_certs(true) disables server-certificate validation.
It is intentionally named as a dangerous operation and should be limited to
controlled troubleshooting. It should not be used in production.
Locale::new(language, country) validates and normalizes its values:
use playhound::Locale;
let locale = Locale::new("ET", "EE")?;
assert_eq!(locale.language.as_str(), "et");
assert_eq!(locale.country.as_str(), "ee");
# Ok::<(), playhound::Error>(())Language values use a nonempty BCP-47-style syntax. Country values must contain exactly two ASCII letters. Locale selection controls the requested storefront language and country; it does not guarantee that every returned field is translated.
Set a default locale on the client and override it on individual request types
with their .locale(...) method.
Supported proxy URL schemes:
http://;https://;socks5://;socks5h://for proxy-side DNS resolution.
Use Proxy::all, Proxy::http, or Proxy::https:
use playhound::{Client, Proxy};
let client = Client::builder()
.proxy(Proxy::http("http://127.0.0.1:8080")?)
.proxy(Proxy::https("socks5h://127.0.0.1:1080")?)
.build()?;
# Ok::<(), playhound::Error>(())With no explicit proxy, Reqwest discovers the standard HTTP_PROXY,
HTTPS_PROXY, ALL_PROXY, and NO_PROXY variables, including lowercase
variants.
Adding any explicit proxy switches the client to the explicit proxy set and disables automatic environment proxy discovery. Scheme-specific rules take precedence over an all-traffic rule. Adding another proxy for the same scope replaces the previous rule.
Call .use_system_proxy(false) to guarantee that environment proxies are not
used:
let client = playhound::Client::builder()
.use_system_proxy(false)
.build()?;
# Ok::<(), playhound::Error>(())Proxy credentials may be embedded in the URL:
http://username:password@proxy.example:8080
socks5h://username:password@proxy.example:1080
PlayHound redacts credentials from proxy Debug output and credential-bearing
transport errors. Avoid putting credentials in source code, shell history, or
committed configuration.
Retries are disabled by default. Enable a bounded exponential policy explicitly:
use playhound::{Client, RetryPolicy};
let client = Client::builder()
.retry_policy(RetryPolicy::exponential(3))
.build()?;
# Ok::<(), playhound::Error>(())Only transient read-only failures are retried: transport failures, rate limits, and service-unavailable responses. Other HTTP statuses, invalid inputs, parsing errors, and upstream response drift are returned immediately.
A valid server Retry-After delay is honored by default and capped by the
configured maximum delay. Set .honor_retry_after(false) to always use local
backoff.
Client-side throttling uses a burst capacity of one:
use std::num::NonZeroU32;
let client = playhound::Client::builder()
.requests_per_second(NonZeroU32::new(2).expect("nonzero"))
.build()?;
# Ok::<(), playhound::Error>(())Rate limiting is per client instance. Clones of a client share its configuration and limiter.
All public result models implement Serialize and Deserialize. They are
marked #[non_exhaustive] so fields and variants can be added compatibly.
Search and collection results contain:
- validated
app_id; - localized
title; - canonical
store_url; - optional icon, developer, and developer ID;
- optional numeric and localized score;
- optional exact
Money; - optional free/paid determination;
- optional localized summary.
AppDetails contains an overview: AppOverview plus optional detailed
metadata:
- plain and HTML descriptions;
- install text and min/max install counts;
- rating/review counts and star histogram;
- availability and in-app-purchase declaration;
- required Android version;
- developer email, website, address, and privacy policy;
- genre and genre ID;
- header image, screenshots, and video;
- content rating;
- release date and update timestamp;
- version and recent changes;
- additional comments.
Optional fields are None when Google omits them. An empty vector represents
an available collection with no elements.
Prices use integer micros to avoid floating-point loss:
let price = playhound::Money::new(
1_990_000,
Some("USD".into()),
Some("$1.99".into()),
);
assert_eq!(price.micros, 1_990_000);
assert!(!price.is_free());
assert_eq!(price.as_major_units(), 1.99);as_major_units() is intended for display. Use micros for exact
comparison and storage.
Review includes the review ID, user display information, timestamp, score,
title/body, optional DeveloperReply, application version, and helpful-vote
count.
Page<T> contains:
items: Vec<T>;next_page_token: Option<PageToken>.
Match individual Error variants when details are needed, or use
Error::kind() for stable broad handling:
use playhound::{Client, Error, ErrorKind};
# async fn example() -> playhound::Result<()> {
let client = Client::new()?;
match client.app("com.example.app").await {
Ok(app) => println!("{}", app.overview.title),
Err(Error::AppNotFound { app_id }) => {
eprintln!("not found: {app_id}");
}
Err(error) if error.kind() == ErrorKind::RateLimited => {
eprintln!("Google Play requested a slower rate");
}
Err(error) => return Err(error),
}
# Ok(())
# }ErrorKind |
Meaning |
|---|---|
InvalidInput |
A request argument failed validation. |
NotFound |
The requested application was confirmed missing. |
RateLimited |
Google Play returned a rate-limit/service-unavailable response. |
HttpStatus |
Another non-success HTTP status was returned. |
Transport |
DNS, connection, TLS, timeout, or body-read failure. |
ResponseTooLarge |
The configured response-size limit was exceeded. |
UnexpectedResponse |
Google returned an unknown response shape. |
Parse |
Recognized response data could not be converted. |
Configuration |
Client construction or configuration failed. |
UnexpectedResponse is deliberately distinct from NotFound: upstream
format drift is not reported as a missing application.
cargo run -- --help
cargo run -- search --json "example query"The -- separates Cargo options from PlayHound options. Cargo may accept some
arguments without it, but using the separator is unambiguous and recommended.
cargo build --release
./target/release/playhound --helpOn Windows:
target\release\playhound.exe --helpcargo install --path .
playhound --helpcargo install playhoundGlobal options can appear before or after a subcommand.
playhound app <APP_ID>
playhound app --lang de --country de <APP_ID>
playhound app --json <APP_ID>Human output is a compact summary. --json emits the complete serialized
AppDetails object.
playhound search [OPTIONS] <QUERY>Search-specific options:
| Option | Values | Default |
|---|---|---|
--limit <N> |
Positive integer | 20 |
--price <PRICE> |
all, free, paid |
all |
Examples:
playhound search "example query"
playhound search --limit 10 --price free "example query"
playhound search --json "example query"JSON output is an array of AppOverview objects.
playhound list [OPTIONS]List-specific options:
| Option | Default |
|---|---|
--collection <VALUE> |
topselling_free |
--category <VALUE> |
APPLICATION |
--age <VALUE> |
No age filter |
--limit <N> |
50 |
Examples:
playhound list
playhound list --collection topselling_paid --category GAME --limit 25
playhound list --category FAMILY --age six-to-eight --jsonCollection and category arguments also accept forward-compatible custom values.
playhound reviews [OPTIONS] <APP_ID>Review-specific options:
| Option | Values | Default |
|---|---|---|
--sort <SORT> |
newest, rating, helpfulness |
newest |
--page-size <N> |
Positive integer | 100 |
--page-token <TOKEN> |
Opaque token | None |
--pages <N> |
Positive integer | 1 |
Examples:
playhound reviews <APP_ID>
playhound reviews --sort helpfulness --page-size 25 <APP_ID>
playhound reviews --page-size 50 --pages 3 --json <APP_ID>
playhound reviews --page-token "<OPAQUE_TOKEN>" --json <APP_ID>The CLI follows at most --pages review pages and stops early when no token is
returned or a token repeats. JSON output is one accumulated array of reviews,
not a Page<Review> wrapper.
playhound suggest "example"
playhound suggest --json "example"JSON output is an array of strings.
playhound check-all \
--app-id <APP_ID> \
--query "example query" \
--limit 3check-all runs app details, search, the default collection, one review page,
and suggestions. It prints one status per operation, or a structured report
with --json. It exits nonzero if any operation fails.
This command is intended for an explicitly manual compatibility check. It contacts the live storefront and is never run by the test suite or CI.
Global CLI options:
| Option | Environment variable | Default | Meaning |
|---|---|---|---|
--lang <CODE> |
PLAYHOUND_LANG |
en |
Response language. |
--country <CODE> |
PLAYHOUND_COUNTRY |
us |
Store country. |
--proxy <URL> |
PLAYHOUND_PROXY |
None | Proxy all traffic. |
--http-proxy <URL> |
PLAYHOUND_HTTP_PROXY |
None | Proxy HTTP traffic. |
--https-proxy <URL> |
PLAYHOUND_HTTPS_PROXY |
None | Proxy HTTPS traffic. |
--no-system-proxy |
PLAYHOUND_NO_SYSTEM_PROXY |
False | Disable environment proxy discovery. |
--requests-per-second <N> |
PLAYHOUND_REQUESTS_PER_SECOND |
Unlimited | Client-side request rate. |
--timeout <SECONDS> |
PLAYHOUND_TIMEOUT |
30 | Complete request timeout. |
--connect-timeout <SECONDS> |
PLAYHOUND_CONNECT_TIMEOUT |
10 | Connection timeout. |
--max-response-bytes <N> |
PLAYHOUND_MAX_RESPONSE_BYTES |
33554432 | Response body limit. |
--retries <N> |
PLAYHOUND_RETRIES |
0 | Maximum transient retries. |
--insecure |
PLAYHOUND_INSECURE |
False | Disable TLS certificate validation. |
--json |
None | False | Emit machine-readable output. |
-v, -vv |
RUST_LOG also supported |
Warnings | Increase diagnostic verbosity. |
Standard HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and NO_PROXY
variables are used only when explicit PlayHound proxy options are absent and
system proxy discovery is enabled.
Example:
PLAYHOUND_COUNTRY=ee \
PLAYHOUND_LANG=et \
PLAYHOUND_PROXY=socks5h://127.0.0.1:1080 \
playhound search --json "example query"Diagnostics are written to stderr. RUST_LOG can select a tracing filter:
RUST_LOG=playhound=debug playhound search "example query"Successful --json output is written to stdout. Errors are written to stderr
as a single JSON object:
{
"error": "application not found: com.example.missing",
"kind": "NotFound",
"ok": false
}CLI exit statuses:
| Status | Meaning |
|---|---|
| 0 | Success |
| 2 | Invalid input or client configuration |
| 3 | Application not found |
| 4 | Rate limited |
| 5 | HTTP, transport, TLS, timeout, or response-size failure |
| 6 | Upstream response drift or parsing failure |
| 1 | Other failure |
Google Play's storefront response formats are not a supported API. A PlayHound
release can stop parsing an operation when the storefront changes. Treat
ErrorKind::UnexpectedResponse as a compatibility problem, not proof that an
application is missing.
Pin a PlayHound version in production automation, monitor failures, and update deliberately.
All limits are maximum requested counts. Google can return fewer items because of storefront caps, locale availability, filtering, exhausted pagination, or response-shape changes.
- Cache data when practical.
- Avoid unnecessary concurrency.
- Configure
requests_per_secondfor batch jobs. - Keep retries bounded.
- Respect rate-limit responses.
- Do not retry parsing or invalid-input failures blindly.
- TLS validation is enabled by default.
- Response bodies are bounded to 32 MiB by default.
- Redirects are limited and restricted to the Google Play host.
- Proxy credentials are redacted from diagnostic formatting.
- No JavaScript from storefront pages is executed.
Tests never contact Google Play or any other network service.
- Parser tests use synthetic wire fixtures.
- Normalized live snapshots are sanitized before being committed.
- Client tests inject in-memory async and blocking transports.
- Property tests feed malformed input to parsers.
- Repository policy tests reject inline production tests and known real fixture identities.
- Every test body lives in a separate file under
tests/.
Run the full suite:
cargo test --workspace --all-features --lockedQuality checks:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --no-default-features --lib --locked
cargo check --no-default-features --locked
cargo check --no-default-features --features blocking --locked
RUSTDOCFLAGS="-Dwarnings" cargo doc --all-features --no-deps --locked
cargo deny check
cargo llvm-cov --workspace --all-features --locked --summary-only
cargo package --all-features --lockedMaintainers can sanitize a normalized live capture with:
cargo run -p playhound-xtask -- sanitize <kind> <input.json> <output.json><kind> is one of app, search, list, reviews, or
suggestions. Raw captures must not be committed.
- Rust edition: 2024.
- Minimum supported Rust version: 1.88.
- CI platforms: Linux, macOS, and Windows.
- TLS backend: Rustls through Reqwest.
The declared MSRV is checked in CI with all features enabled.
PlayHound is licensed under the European Union Public Licence 1.2
(EUPL-1.2). See the LICENSE file distributed with the project and crate.