@mradex77/google-play-scraper 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MrAdex77
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,508 @@
1
+ # @mradex77/google-play-scraper
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@mradex77/google-play-scraper.svg)](https://www.npmjs.com/package/@mradex77/google-play-scraper)
4
+ [![CI](https://github.com/MrAdex77/google-play-scraper/actions/workflows/ci.yml/badge.svg)](https://github.com/MrAdex77/google-play-scraper/actions/workflows/ci.yml)
5
+ [![Live contract tests](https://github.com/MrAdex77/google-play-scraper/actions/workflows/e2e.yml/badge.svg)](https://github.com/MrAdex77/google-play-scraper/actions/workflows/e2e.yml)
6
+ [![license](https://img.shields.io/npm/l/@mradex77/google-play-scraper.svg)](LICENSE)
7
+
8
+ A modern TypeScript rewrite of the unmaintained [`google-play-scraper`](https://github.com/facundoolano/google-play-scraper). It scrapes public app data from Google Play — app details, search, suggestions, lists, developer pages, similar apps, reviews, permissions, and data safety. Ships as both ESM and CommonJS, returns fully typed results validated with [zod](https://zod.dev), runs on native `fetch` with no runtime HTTP dependency, and isolates every fragile Google Play array path behind a resilient spec layer that is exercised by daily live contract tests.
9
+
10
+ The public method names, options, and constants match the original library, so migrating is mostly a matter of swapping the import.
11
+
12
+ ## Installation
13
+
14
+ ```
15
+ npm install @mradex77/google-play-scraper
16
+ ```
17
+
18
+ Requires Node.js 22.12 or newer.
19
+
20
+ ## Quick start
21
+
22
+ The package exposes named exports and an aggregate default export. Use whichever style you prefer.
23
+
24
+ ```typescript
25
+ import gplay from '@mradex77/google-play-scraper';
26
+
27
+ const app = await gplay.app({ appId: 'com.google.android.apps.translate' });
28
+ console.log(app.title, app.score);
29
+ ```
30
+
31
+ ```typescript
32
+ import { app, type App } from '@mradex77/google-play-scraper';
33
+
34
+ const details: App = await app({ appId: 'com.google.android.apps.translate' });
35
+ console.log(details.installs);
36
+ ```
37
+
38
+ CommonJS works too:
39
+
40
+ ```javascript
41
+ const gplay = require('@mradex77/google-play-scraper').default;
42
+
43
+ gplay.search({ term: 'panda' }).then((results) => {
44
+ console.log(results.length);
45
+ });
46
+ ```
47
+
48
+ ## Common options
49
+
50
+ Every method accepts a single options object. These options are available on all of them:
51
+
52
+ | Option | Type | Default | Description |
53
+ | ---------------- | -------- | ------- | ------------------------------------------------------------------------------------ |
54
+ | `lang` | `string` | `'en'` | Two letter language code used to fetch the page. |
55
+ | `country` | `string` | `'us'` | Two letter country code. Needed for apps available only in some regions. |
56
+ | `throttle` | `number` | none | Maximum requests per second across a single call. |
57
+ | `requestOptions` | `object` | none | HTTP overrides. See [Throttling and requestOptions](#throttling-and-requestoptions). |
58
+
59
+ ## Methods
60
+
61
+ - [app](#app) — full detail of a single application
62
+ - [search](#search) — apps matching a search term
63
+ - [suggest](#suggest) — search term autocompletions
64
+ - [list](#list) — a ranked collection of apps
65
+ - [developer](#developer) — other apps by the same developer
66
+ - [similar](#similar) — apps related to a given app
67
+ - [reviews](#reviews) — user reviews for an app
68
+ - [permissions](#permissions) — permissions an app requests
69
+ - [datasafety](#datasafety) — the data safety section of an app
70
+ - [categories](#categories) — the Google Play category taxonomy
71
+ - [memoized](#memoized) — a client that caches identical calls
72
+
73
+ ### app
74
+
75
+ Retrieves the full detail of an application.
76
+
77
+ | Option | Type | Default | Description |
78
+ | ------- | -------- | -------- | ---------------------------------------------- |
79
+ | `appId` | `string` | required | The Google Play id (the `?id=` url parameter). |
80
+
81
+ ```typescript
82
+ import { app } from '@mradex77/google-play-scraper';
83
+
84
+ const details = await app({ appId: 'com.google.android.apps.translate' });
85
+ ```
86
+
87
+ Returns an `App` with 55 fields. Trimmed:
88
+
89
+ ```javascript
90
+ {
91
+ title: 'Google Translate',
92
+ description: 'Translate between up to 133 languages...',
93
+ descriptionHTML: 'Translate between up to 133 languages...<br>...',
94
+ summary: 'Instantly translate text, speech and images in over 100 languages',
95
+ installs: '1,000,000,000+',
96
+ minInstalls: 1000000000,
97
+ score: 4.48,
98
+ scoreText: '4.5',
99
+ ratings: 8765432,
100
+ reviews: 5678901,
101
+ histogram: { '1': 123456, '2': 45678, '3': 90123, '4': 234567, '5': 4567890 },
102
+ price: 0,
103
+ free: true,
104
+ currency: 'USD',
105
+ priceText: 'Free',
106
+ available: true,
107
+ offersIAP: false,
108
+ androidVersion: '8.0',
109
+ androidVersionText: '8.0 and up',
110
+ developer: 'Google LLC',
111
+ developerId: '5700313618786177705',
112
+ developerEmail: 'apps-help@google.com',
113
+ developerWebsite: 'http://support.google.com/translate',
114
+ genre: 'Tools',
115
+ genreId: 'TOOLS',
116
+ categories: [{ name: 'Tools', id: 'TOOLS' }],
117
+ icon: 'https://play-lh.googleusercontent.com/...',
118
+ screenshots: ['https://play-lh.googleusercontent.com/...'],
119
+ contentRating: 'Everyone',
120
+ adSupported: false,
121
+ updated: 1719878400000,
122
+ version: 'Varies with device',
123
+ comments: [],
124
+ appId: 'com.google.android.apps.translate',
125
+ url: 'https://play.google.com/store/apps/details?id=com.google.android.apps.translate'
126
+ }
127
+ ```
128
+
129
+ ### search
130
+
131
+ Retrieves apps that match a search term.
132
+
133
+ | Option | Type | Default | Description |
134
+ | ------------ | --------------------------- | -------- | ------------------------------------------------------------- |
135
+ | `term` | `string` | required | The search query. |
136
+ | `num` | `number` | `20` | Number of results, up to `250`. |
137
+ | `price` | `'all' \| 'free' \| 'paid'` | `'all'` | Filter results by price. |
138
+ | `fullDetail` | `boolean` | `false` | When `true`, fetch and return the full `App` for each result. |
139
+
140
+ ```typescript
141
+ import { search } from '@mradex77/google-play-scraper';
142
+
143
+ const results = await search({ term: 'panda', num: 5 });
144
+ ```
145
+
146
+ Returns `SearchResult[]` (or `App[]` when `fullDetail` is `true`). Trimmed:
147
+
148
+ ```javascript
149
+ [
150
+ {
151
+ title: 'Panda VPN',
152
+ appId: 'com.example.pandavpn',
153
+ url: 'https://play.google.com/store/apps/details?id=com.example.pandavpn',
154
+ icon: 'https://play-lh.googleusercontent.com/...',
155
+ developer: 'Panda Labs',
156
+ developerId: '1234567890',
157
+ currency: 'USD',
158
+ price: 0,
159
+ free: true,
160
+ summary: 'Fast and secure VPN',
161
+ scoreText: '4.3',
162
+ score: 4.3,
163
+ },
164
+ ];
165
+ ```
166
+
167
+ ### suggest
168
+
169
+ Given a partial term, returns up to five search completions.
170
+
171
+ | Option | Type | Default | Description |
172
+ | ------ | -------- | -------- | ------------------------- |
173
+ | `term` | `string` | required | The partial search query. |
174
+
175
+ ```typescript
176
+ import { suggest } from '@mradex77/google-play-scraper';
177
+
178
+ const suggestions = await suggest({ term: 'pand' });
179
+ ```
180
+
181
+ Returns `string[]`:
182
+
183
+ ```javascript
184
+ ['panda', 'pandora', 'panda vpn', 'panda pop', 'pandora music'];
185
+ ```
186
+
187
+ ### list
188
+
189
+ Retrieves a ranked collection of apps, optionally scoped to a category and an age bracket.
190
+
191
+ | Option | Type | Default | Description |
192
+ | ------------ | ------------ | ---------------------- | -------------------------------------------------------- |
193
+ | `collection` | `Collection` | `collection.TOP_FREE` | One of `TOP_FREE`, `TOP_PAID`, `GROSSING`. |
194
+ | `category` | `Category` | `category.APPLICATION` | Any [category](#constants) constant. |
195
+ | `age` | `Age` | none | One of `age.FIVE_UNDER`, `age.SIX_EIGHT`, `age.NINE_UP`. |
196
+ | `num` | `number` | `500` | Number of results. |
197
+ | `fullDetail` | `boolean` | `false` | When `true`, return the full `App` for each result. |
198
+
199
+ ```typescript
200
+ import { list, collection, category } from '@mradex77/google-play-scraper';
201
+
202
+ const items = await list({
203
+ collection: collection.TOP_FREE,
204
+ category: category.GAME,
205
+ num: 5,
206
+ });
207
+ ```
208
+
209
+ Returns `ListItem[]` (or `App[]` when `fullDetail` is `true`), each shaped like a [search](#search) result.
210
+
211
+ ### developer
212
+
213
+ Returns other apps published by the same developer. The `devId` is either the numeric developer id or the developer name, exactly as it appears on Google Play.
214
+
215
+ | Option | Type | Default | Description |
216
+ | ------------ | --------- | -------- | --------------------------------------------------- |
217
+ | `devId` | `string` | required | Numeric developer id or developer name. |
218
+ | `num` | `number` | `60` | Number of results. |
219
+ | `fullDetail` | `boolean` | `false` | When `true`, return the full `App` for each result. |
220
+
221
+ ```typescript
222
+ import { developer } from '@mradex77/google-play-scraper';
223
+
224
+ const apps = await developer({ devId: '5700313618786177705' });
225
+ ```
226
+
227
+ Returns `DeveloperApp[]` (or `App[]` when `fullDetail` is `true`), each shaped like a [search](#search) result.
228
+
229
+ ### similar
230
+
231
+ Returns apps related to a given app.
232
+
233
+ | Option | Type | Default | Description |
234
+ | ------------ | --------- | -------- | --------------------------------------------------- |
235
+ | `appId` | `string` | required | The Google Play id of the reference app. |
236
+ | `fullDetail` | `boolean` | `false` | When `true`, return the full `App` for each result. |
237
+
238
+ ```typescript
239
+ import { similar } from '@mradex77/google-play-scraper';
240
+
241
+ const apps = await similar({ appId: 'com.google.android.apps.translate' });
242
+ ```
243
+
244
+ Returns `SimilarApp[]` (or `App[]` when `fullDetail` is `true`), each shaped like a [search](#search) result.
245
+
246
+ ### reviews
247
+
248
+ Retrieves reviews for an app. Reviews always come back inside a `{ data, nextPaginationToken }` envelope so paging is uniform.
249
+
250
+ | Option | Type | Default | Description |
251
+ | --------------------- | --------- | ------------- | -------------------------------------------------------- |
252
+ | `appId` | `string` | required | The Google Play id of the app. |
253
+ | `sort` | `Sort` | `sort.NEWEST` | One of `sort.NEWEST`, `sort.RATING`, `sort.HELPFULNESS`. |
254
+ | `num` | `number` | `150` | Number of reviews to fetch. |
255
+ | `paginate` | `boolean` | `false` | When `true`, fetch a single page and return its token. |
256
+ | `nextPaginationToken` | `string` | none | Continue from a token returned by a previous call. |
257
+
258
+ ```typescript
259
+ import { reviews, sort } from '@mradex77/google-play-scraper';
260
+
261
+ const first = await reviews({ appId: 'com.google.android.apps.translate', sort: sort.NEWEST });
262
+
263
+ if (first.nextPaginationToken) {
264
+ const next = await reviews({
265
+ appId: 'com.google.android.apps.translate',
266
+ paginate: true,
267
+ nextPaginationToken: first.nextPaginationToken,
268
+ });
269
+ }
270
+ ```
271
+
272
+ Returns `ReviewsResult`. Trimmed:
273
+
274
+ ```javascript
275
+ {
276
+ data: [
277
+ {
278
+ id: 'gp:AOqpTOH...',
279
+ userName: 'Ada Lovelace',
280
+ userImage: 'https://play-lh.googleusercontent.com/...',
281
+ date: '2026-06-30T12:00:00.000Z',
282
+ score: 5,
283
+ title: null,
284
+ text: 'Works offline and the camera translation is great.',
285
+ thumbsUp: 42,
286
+ version: '8.9.0',
287
+ criterias: []
288
+ }
289
+ ],
290
+ nextPaginationToken: 'CqYBCqMB...'
291
+ }
292
+ ```
293
+
294
+ ### permissions
295
+
296
+ Returns the permissions an app requests.
297
+
298
+ | Option | Type | Default | Description |
299
+ | ------- | --------- | -------- | ----------------------------------------------------------------- |
300
+ | `appId` | `string` | required | The Google Play id of the app. |
301
+ | `short` | `boolean` | `false` | When `true`, return a flat `string[]` of common permission names. |
302
+
303
+ ```typescript
304
+ import { permissions } from '@mradex77/google-play-scraper';
305
+
306
+ const detailed = await permissions({ appId: 'com.google.android.apps.translate' });
307
+ const names = await permissions({ appId: 'com.google.android.apps.translate', short: true });
308
+ ```
309
+
310
+ Returns `AppPermission[]` (or `string[]` when `short` is `true`):
311
+
312
+ ```javascript
313
+ [
314
+ { permission: 'take pictures and videos', type: 0 },
315
+ { permission: 'view network connections', type: 1 },
316
+ ];
317
+ ```
318
+
319
+ The `type` is `permission.COMMON` (`0`) or `permission.OTHER` (`1`).
320
+
321
+ ### datasafety
322
+
323
+ Returns the data safety section of an app.
324
+
325
+ | Option | Type | Default | Description |
326
+ | ------- | -------- | -------- | ------------------------------ |
327
+ | `appId` | `string` | required | The Google Play id of the app. |
328
+
329
+ ```typescript
330
+ import { datasafety } from '@mradex77/google-play-scraper';
331
+
332
+ const safety = await datasafety({ appId: 'com.google.android.apps.translate' });
333
+ ```
334
+
335
+ Returns `DataSafety`. Trimmed:
336
+
337
+ ```javascript
338
+ {
339
+ sharedData: [
340
+ { data: 'Approximate location', optional: false, purpose: 'App functionality', type: 'Location' }
341
+ ],
342
+ collectedData: [
343
+ { data: 'Email address', optional: false, purpose: 'Account management', type: 'Personal info' }
344
+ ],
345
+ securityPractices: [
346
+ { practice: 'Data is encrypted in transit', description: 'Your data is transferred over a secure connection' }
347
+ ],
348
+ privacyPolicyUrl: 'https://policies.google.com/privacy'
349
+ }
350
+ ```
351
+
352
+ ### categories
353
+
354
+ Returns the Google Play category taxonomy as a list of category ids.
355
+
356
+ | Option | Type | Default | Description |
357
+ | ---------------- | -------- | ------- | -------------------------------------- |
358
+ | `throttle` | `number` | none | See [common options](#common-options). |
359
+ | `requestOptions` | `object` | none | See [common options](#common-options). |
360
+
361
+ ```typescript
362
+ import { categories } from '@mradex77/google-play-scraper';
363
+
364
+ const ids = await categories();
365
+ ```
366
+
367
+ Returns `string[]`:
368
+
369
+ ```javascript
370
+ [
371
+ 'APPLICATION',
372
+ 'ANDROID_WEAR',
373
+ 'ART_AND_DESIGN',
374
+ 'AUTO_AND_VEHICLES',
375
+ 'GAME',
376
+ 'GAME_ACTION',
377
+ 'FAMILY',
378
+ ];
379
+ ```
380
+
381
+ ### memoized
382
+
383
+ Returns a client whose methods share an in-memory LRU cache, so identical calls made within the TTL resolve from cache instead of hitting Google Play again.
384
+
385
+ | Option | Type | Default | Description |
386
+ | ---------- | -------- | -------- | ---------------------------------------------- |
387
+ | `maxAgeMs` | `number` | `300000` | Time to live per cache entry, in milliseconds. |
388
+ | `max` | `number` | `1000` | Maximum number of cached entries. |
389
+
390
+ ```typescript
391
+ import { memoized } from '@mradex77/google-play-scraper';
392
+
393
+ const client = memoized({ maxAgeMs: 60000, max: 500 });
394
+
395
+ await client.app({ appId: 'com.google.android.apps.translate' });
396
+ await client.app({ appId: 'com.google.android.apps.translate' });
397
+ ```
398
+
399
+ The returned client exposes every method above plus the exported constants.
400
+
401
+ ## Constants
402
+
403
+ The library re-exports the same constant sets as the original, frozen and typed.
404
+
405
+ | Constant | Values |
406
+ | ------------ | ----------------------------------------------------------------------------------------------------------------- |
407
+ | `category` | All app and game categories plus the `FAMILY` set (e.g. `APPLICATION`, `TOOLS`, `GAME`, `GAME_PUZZLE`, `FAMILY`). |
408
+ | `collection` | `TOP_FREE`, `TOP_PAID`, `GROSSING`. |
409
+ | `sort` | `NEWEST` (`2`), `RATING` (`3`), `HELPFULNESS` (`1`). |
410
+ | `age` | `FIVE_UNDER` (`'AGE_RANGE1'`), `SIX_EIGHT` (`'AGE_RANGE2'`), `NINE_UP` (`'AGE_RANGE3'`). |
411
+ | `permission` | `COMMON` (`0`), `OTHER` (`1`). |
412
+
413
+ ```typescript
414
+ import { category, collection, sort, age, permission } from '@mradex77/google-play-scraper';
415
+ ```
416
+
417
+ ## Error handling
418
+
419
+ Every failure surfaces as a typed subclass of `GooglePlayError`, so you can branch on the exact cause instead of parsing message strings.
420
+
421
+ | Error | Extends | Thrown when |
422
+ | ----------------- | ----------------- | ----------------------------------------------------------------------------------------- |
423
+ | `GooglePlayError` | `Error` | Base class for every error the library throws. |
424
+ | `ValidationError` | `GooglePlayError` | The options you passed fail their zod schema. |
425
+ | `HttpError` | `GooglePlayError` | A request fails with a non-success status or a network error. Carries `status` and `url`. |
426
+ | `NotFoundError` | `HttpError` | Google Play responds `404`, e.g. an unknown `appId`. |
427
+ | `RateLimitError` | `HttpError` | Google Play responds `429` after retries are exhausted. |
428
+ | `BlockedError` | `GooglePlayError` | A consent wall or captcha interstitial is detected. |
429
+ | `ParseError` | `GooglePlayError` | A batchexecute response cannot be parsed. |
430
+ | `SpecError` | `ParseError` | Extraction fails; lists every failing field and the paths that were tried. |
431
+
432
+ ```typescript
433
+ import { app, NotFoundError, SpecError } from '@mradex77/google-play-scraper';
434
+
435
+ try {
436
+ const details = await app({ appId: 'com.does.not.exist' });
437
+ } catch (error) {
438
+ if (error instanceof NotFoundError) {
439
+ console.error('No such app');
440
+ } else if (error instanceof SpecError) {
441
+ console.error('Google Play changed its layout:', error.failures);
442
+ } else {
443
+ throw error;
444
+ }
445
+ }
446
+ ```
447
+
448
+ ## Throttling and requestOptions
449
+
450
+ Pass `throttle` to cap requests per second, and `requestOptions` to override the HTTP layer:
451
+
452
+ | requestOptions field | Type | Description |
453
+ | -------------------- | ------------------------ | -------------------------------------------------------------- |
454
+ | `headers` | `Record<string, string>` | Extra headers merged into every request. |
455
+ | `fetchImpl` | `typeof fetch` | A custom `fetch` implementation, useful for proxies and tests. |
456
+ | `timeoutMs` | `number` | Per-request timeout, up to `120000`. Default `30000`. |
457
+ | `retries` | `number` | Retry count for `429` and `5xx`, `0` to `5`. Default `2`. |
458
+
459
+ ```typescript
460
+ import { app } from '@mradex77/google-play-scraper';
461
+
462
+ const details = await app({
463
+ appId: 'com.google.android.apps.translate',
464
+ throttle: 5,
465
+ requestOptions: {
466
+ timeoutMs: 15000,
467
+ retries: 3,
468
+ headers: { 'Accept-Language': 'de' },
469
+ fetchImpl: myProxiedFetch,
470
+ },
471
+ });
472
+ ```
473
+
474
+ Retries use exponential backoff and honor a `Retry-After` header when present.
475
+
476
+ ## Resilience
477
+
478
+ Google Play serves its data as deeply nested, unlabeled arrays whose positions shift a few times a year. That is what breaks scrapers. Every positional path in this library lives as a typed constant in a per-feature `specs.ts` file, never inline in logic, and each field is resolved through an ordered list of candidate paths so a single moved index does not take the whole call down. Extraction collects all field failures in one pass and throws a single `SpecError` naming every broken field and the paths that were tried, which is exactly the input the maintenance runbook needs. Unknown data enters as `unknown` and only leaves through a zod schema, so a layout change fails loudly at the boundary rather than three layers up.
479
+
480
+ To catch breakage before users do, the `e2e/` suite runs against live Google Play on a daily GitHub Actions schedule and opens a labeled issue on failure. Repairing a break is a one-file spec diff, walked through step by step in [docs/RUNBOOK.md](docs/RUNBOOK.md).
481
+
482
+ ## Migrating from google-play-scraper
483
+
484
+ The method names, options, and constants are the same, so most code keeps working after swapping the import. Watch for these differences:
485
+
486
+ - `reviews` always returns the `{ data, nextPaginationToken }` envelope, never a bare array.
487
+ - Dates are ISO 8601 strings (review `date`, `replyDate`), and `updated` is a millisecond timestamp.
488
+ - Errors are the typed classes above instead of plain `Error`.
489
+ - The package is ESM-first with a CommonJS build; the default export is the aggregate client and named exports are also available.
490
+
491
+ ## Contributing
492
+
493
+ This project uses [pnpm](https://pnpm.io), [Conventional Commits](https://www.conventionalcommits.org), and the conventional branch spec (`feature/`, `bugfix/`, `chore/`, ...). Versioning, changelog, and npm publishing are automated with Release Please.
494
+
495
+ ```
496
+ pnpm install
497
+ pnpm lint eslint on the whole repo
498
+ pnpm typecheck tsc --noEmit
499
+ pnpm test unit tests, offline against recorded fixtures
500
+ pnpm test:coverage unit tests with coverage thresholds
501
+ pnpm test:e2e live contract tests against play.google.com
502
+ pnpm build emit dist/ with esm, cjs, and d.ts
503
+ pnpm check:package build then verify the published package
504
+ ```
505
+
506
+ ## License
507
+
508
+ MIT