google-play-scraper
Advanced tools
Comparing version
@@ -132,4 +132,6 @@ // constants | ||
genreId: string | ||
familyGenre: string | ||
familyGenreId: string | ||
categories: Array<{ | ||
name: string | ||
id: string|null | ||
}> | ||
icon: string | ||
@@ -204,3 +206,3 @@ headerImage: string | ||
export interface IFnList { | ||
(options?: IFnListOptions): Promise<IAppItem[]> | ||
<T extends IFnListOptions>(options: T): T extends { fullDetail: true } ? Promise<IAppItemFullDetail[]> : Promise<IAppItem[]> | ||
} | ||
@@ -219,3 +221,3 @@ | ||
export interface IFnSearch { | ||
(options: IFnSearchOptions): Promise<IAppItem[]> | ||
<T extends IFnSearchOptions>(options: T): T extends { fullDetail: true } ? Promise<IAppItemFullDetail[]> : Promise<IAppItem[]> | ||
} | ||
@@ -233,3 +235,3 @@ | ||
export interface IFnDeveloper { | ||
(options: IFnDeveloperOptions): Promise<IAppItem[]> | ||
<T extends IFnDeveloperOptions>(options: T): T extends { fullDetail: true } ? Promise<IAppItemFullDetail[]> : Promise<IAppItem[]> | ||
} | ||
@@ -272,3 +274,3 @@ | ||
export interface IFnSimilar { | ||
(options: IFnReviewsOptions): Promise<IAppItem[]> | ||
<T extends IFnSimilarOptions>(options: T): T extends { fullDetail: true } ? Promise<IAppItemFullDetail[]> : Promise<IAppItem[]> | ||
} | ||
@@ -275,0 +277,0 @@ |
@@ -47,6 +47,9 @@ 'use strict'; | ||
description: { | ||
path: ['ds:5', 1, 2, 72, 0, 1], | ||
fun: helper.descriptionText | ||
path: ['ds:5', 1, 2], | ||
fun: (val) => helper.descriptionText(helper.descriptionHtmlLocalized(val)) | ||
}, | ||
descriptionHTML: ['ds:5', 1, 2, 72, 0, 1], | ||
descriptionHTML: { | ||
path: ['ds:5', 1, 2], | ||
fun: helper.descriptionHtmlLocalized | ||
}, | ||
summary: ['ds:5', 1, 2, 73, 0, 1], | ||
@@ -68,2 +71,8 @@ installs: ['ds:5', 1, 2, 13, 0], | ||
}, | ||
// If there is a discount, originalPrice if filled. | ||
originalPrice: { | ||
path: ['ds:5', 1, 2, 57, 0, 0, 0, 0, 1, 1, 0], | ||
fun: (price) => price ? price / 1000000 : undefined | ||
}, | ||
discountEndDate: ['ds:5', 1, 2, 57, 0, 0, 0, 0, 14, 1], | ||
free: { | ||
@@ -96,2 +105,6 @@ path: ['ds:5', 1, 2, 57, 0, 0, 0, 0, 1, 0, 0], | ||
}, | ||
androidMaxVersion: { | ||
path: ['ds:5', 1, 2, 140, 1, 1, 0, 1, 1], | ||
fun: helper.normalizeAndroidVersion | ||
}, | ||
developer: ['ds:5', 1, 2, 68, 0], | ||
@@ -112,4 +125,16 @@ developerId: { | ||
genreId: ['ds:5', 1, 2, 79, 0, 0, 2], | ||
familyGenre: ['ds:5', 0, 12, 13, 1, 0], | ||
familyGenreId: ['ds:5', 0, 12, 13, 1, 2], | ||
categories: { | ||
path: ['ds:5', 1, 2], | ||
fun: (searchArray) => { | ||
const categories = helper.extractCategories(R.path([118], searchArray)); | ||
if (categories.length === 0) { | ||
// add genre and genreId like GP does when there're no categories available | ||
categories.push({ | ||
name: R.path([79, 0, 0, 0], searchArray), | ||
id: R.path([79, 0, 0, 2], searchArray) | ||
}); | ||
} | ||
return categories; | ||
} | ||
}, | ||
icon: ['ds:5', 1, 2, 95, 0, 3, 2], | ||
@@ -126,2 +151,3 @@ headerImage: ['ds:5', 1, 2, 96, 0, 3, 2], | ||
videoImage: ['ds:5', 1, 2, 100, 1, 0, 3, 2], | ||
previewVideo: ['ds:5', 1, 2, 100, 1, 2, 0, 2], | ||
contentRating: ['ds:5', 1, 2, 9, 0], | ||
@@ -144,8 +170,21 @@ contentRatingDescription: ['ds:5', 1, 2, 9, 2, 1], | ||
comments: { | ||
path: ['ds:9', 0], | ||
path: ['ds:8', 0], | ||
isArray: true, | ||
fun: helper.extractComments | ||
}, | ||
preregister: { | ||
path: ['ds:5', 1, 2, 18, 0], | ||
fun: (val) => val === 1 | ||
}, | ||
earlyAccessEnabled: { | ||
path: ['ds:5', 1, 2, 18, 2], | ||
fun: (val) => typeof val === 'string' | ||
}, | ||
isAvailableInPlayPass: { | ||
path: ['ds:5', 1, 2, 62], | ||
fun: (field) => !!field | ||
} | ||
}; | ||
module.exports = app; |
@@ -116,3 +116,3 @@ 'use strict'; | ||
const sectionTitle = R.path(SECTIONS_MAPPING.title, section); | ||
return R.is(String, sectionTitle) && R.isEmpty(sectionTitle); | ||
return R.is(String, sectionTitle); | ||
} | ||
@@ -183,3 +183,3 @@ | ||
if (opts.num && opts.num > 250) { | ||
throw Error("The number of results can't exceed 250"); | ||
throw Error('The number of results can\'t exceed 250'); | ||
} | ||
@@ -186,0 +186,0 @@ |
const cheerio = require('cheerio'); | ||
const R = require('ramda'); | ||
function descriptionHtmlLocalized (searchArray) { | ||
const descriptionTranslation = R.path([12, 0, 0, 1], searchArray); | ||
const descriptionOriginal = R.path([72, 0, 1], searchArray); | ||
return descriptionTranslation || descriptionOriginal; | ||
} | ||
function descriptionText (description) { | ||
@@ -61,3 +68,25 @@ // preserve the line breaks when converting to text | ||
/** | ||
* Recursively extracts the categories of the App | ||
* @param {array} categories The categories array | ||
*/ | ||
function extractCategories (searchArray, categories = []) { | ||
if (searchArray === null || searchArray.length === 0) return categories; | ||
if (searchArray.length >= 4 && typeof searchArray[0] === 'string') { | ||
categories.push({ | ||
name: searchArray[0], | ||
id: searchArray[2] | ||
}); | ||
} else { | ||
searchArray.forEach((sub) => { | ||
extractCategories(sub, categories); | ||
}); | ||
} | ||
return categories; | ||
} | ||
module.exports = { | ||
descriptionHtmlLocalized, | ||
descriptionText, | ||
@@ -68,3 +97,4 @@ priceText, | ||
extractComments, | ||
extractFeatures | ||
extractFeatures, | ||
extractCategories | ||
}; |
@@ -29,3 +29,3 @@ function sleep (ms) { | ||
* @return result of the executed function from parent settingOption @function | ||
*/ | ||
*/ | ||
return async function returnedFunction (...args) { | ||
@@ -32,0 +32,0 @@ // Set Date Variable if it's Empty |
{ | ||
"name": "google-play-scraper", | ||
"version": "9.1.1", | ||
"version": "9.2.0", | ||
"description": "scrapes app data from google play store", | ||
@@ -27,4 +27,4 @@ "main": "index.js", | ||
"cheerio": "^1.0.0-rc.10", | ||
"debug": "^2.2.0", | ||
"got": "^11.8.3", | ||
"debug": "^3.1.0", | ||
"got": "^11.8.6", | ||
"memoizee": "^0.4.11", | ||
@@ -44,4 +44,5 @@ "ramda": "^0.21.0" | ||
"promise-log": "^0.1.0", | ||
"sinon": "^15.2.0", | ||
"validator": "^13.7.0" | ||
} | ||
} |
@@ -69,2 +69,3 @@ # google-play-scraper [](https://github.com/facundoolano/google-play-scraper/actions/workflows/tests.yml) | ||
androidVersionText: 'Varies with device', | ||
androidMaxVersion: 'VARY', | ||
developer: 'Google LLC', | ||
@@ -79,4 +80,6 @@ developerId: '5700313618786177705', | ||
genreId: 'TOOLS', | ||
familyGenre: undefined, | ||
familyGenreId: undefined, | ||
categories: [ | ||
{ name: 'Tools', id: 'TOOLS' }, | ||
{ name: 'Another category without id', id: null } | ||
], | ||
icon: 'https://lh3.googleusercontent.com/ZrNeuKthBirZN7rrXPN1JmUbaG8ICy3kZSHt-WgSnREsJzo2txzCzjIoChlevMIQEA', | ||
@@ -90,2 +93,3 @@ headerImage: 'https://lh3.googleusercontent.com/e4Sfy0cOmqpike76V6N6n-tDVbtbmt6MxbnbkKBZ_7hPHZRfsCeZhMBZK8eFDoDa1Vf-', | ||
videoImage: undefined, | ||
previewVideo: undefined, | ||
contentRating: 'Everyone', | ||
@@ -99,2 +103,5 @@ contentRatingDescription: undefined, | ||
comments: [], | ||
preregister: false, | ||
earlyAccessEnabled: false, | ||
isAvailableInPlayPass: false, | ||
editorsChoice: true, | ||
@@ -112,3 +119,4 @@ features: [ | ||
appId: 'com.google.android.apps.translate', | ||
url: 'https://play.google.com/store/apps/details?id=com.google.android.apps.translate&hl=en&gl=us' | ||
url: 'https://play.google.com/store/apps/details?id=com.google.android.apps.translate&hl=en&gl=us', | ||
isAvailableInPlayPass: false | ||
} | ||
@@ -120,4 +128,4 @@ ``` | ||
* `collection` (optional, defaults to `collection.TOP_FREE`): the Google Play collection that will be retrieved. Available options can bee found [here](https://github.com/facundoolano/google-play-scraper/blob/dev/lib/constants.js#L58). | ||
* `category` (optional, defaults to no category): the app category to filter by. Available options can bee found [here](https://github.com/facundoolano/google-play-scraper/blob/dev/lib/constants.js#L3). | ||
* `collection` (optional, defaults to `collection.TOP_FREE`): the Google Play collection that will be retrieved. Available options can bee found [here](https://github.com/facundoolano/google-play-scraper/blob/b7669f78766b8306896447ddbe8797fe36eae49f/lib/constants.js#L67). | ||
* `category` (optional, defaults to no category): the app category to filter by. Available options can bee found [here](https://github.com/facundoolano/google-play-scraper/blob/b7669f78766b8306896447ddbe8797fe36eae49f/lib/constants.js#L10). | ||
* `age` (optional, defaults to no age filter): the age range to filter the apps (only for FAMILY and its subcategories). Available options are `age.FIVE_UNDER`, `age.SIX_EIGHT`, `age.NINE_UP`. | ||
@@ -313,3 +321,3 @@ * `num` (optional, defaults to 500): the amount of apps to retrieve. | ||
gplay.reviews({ | ||
appId: 'com.mojang.minecraftpe', | ||
appId: 'com.dxco.pandavszombies', | ||
sort: gplay.sort.RATING, | ||
@@ -323,3 +331,3 @@ num: 3000 | ||
gplay.reviews({ | ||
appId: 'com.mojang.minecraftpe', | ||
appId: 'com.dxco.pandavszombies', | ||
sort: gplay.sort.RATING, | ||
@@ -334,3 +342,3 @@ paginate: true, | ||
gplay.reviews({ | ||
appId: 'com.mojang.minecraftpe', | ||
appId: 'com.dxco.pandavszombies', | ||
sort: gplay.sort.RATING, | ||
@@ -337,0 +345,0 @@ paginate: true, |
@@ -12,2 +12,4 @@ 'use strict'; | ||
assert.isBoolean(app.isAvailableInPlayPass); | ||
assert.isNumber(app.score); | ||
@@ -25,5 +27,9 @@ assert(app.score > 0); | ||
assert.equal(app.genreId, 'GAME_PUZZLE'); | ||
assert.equal(app.familyGenre, undefined); | ||
assert.equal(app.familyGenreId, undefined); | ||
assert.isArray(app.categories); | ||
assert.isAbove(app.categories.length, 1); | ||
assert.equal(app.categories[0].id, 'GAME_PUZZLE'); | ||
assert.notEqual(app.categories[1].id, 'GAME_PUZZLE'); | ||
assert.hasAllKeys(app.categories[0], ['name', 'id']); | ||
assert.isString(app.version); | ||
@@ -36,2 +42,3 @@ if (app.size) { | ||
assert.equal(app.androidVersion, '7.0'); | ||
assert.equal(app.androidMaxVersion, 'VARY'); | ||
@@ -44,3 +51,6 @@ assert.isBoolean(app.available); | ||
assert.isString(app.IAPRange); | ||
// assert(app.preregister === false); | ||
assert.isFalse(app.preregister); | ||
assert.isFalse(app.earlyAccessEnabled); | ||
assert.isUndefined(app.originalPrice); | ||
assert.isUndefined(app.discountEndDate); | ||
@@ -54,2 +64,3 @@ assert.equal(app.developer, 'Jam City, Inc.'); | ||
assertValidUrl(app.video); | ||
assertValidUrl(app.previewVideo); | ||
['1', '2', '3', '4', '5'].map((v) => assert.property(app.histogram, v)); | ||
@@ -119,3 +130,5 @@ | ||
.then((app) => { | ||
assert.equal(app.developerAddress, '63 Market St.\nVenice CA, 90291'); | ||
assert.equal(app.developerAddress, '2772 Donald Douglas Loop, North\n' + | ||
'Santa Monica, CA 90405\n' + | ||
'USA'); | ||
}); | ||
@@ -185,3 +198,3 @@ }); | ||
.then((app) => { | ||
assert.equal(app.developerInternalID, '6289421402968163029'); | ||
assert.equal(app.developerInternalID, '9028773071151690823'); | ||
}); | ||
@@ -196,2 +209,10 @@ }); | ||
}); | ||
it('should fetch android version limit set for some old apps', () => { | ||
return gplay.app({ appId: 'air.com.zinkia.playset' }) | ||
.then((app) => { | ||
assert.equal(app.androidVersion, '4.2'); | ||
assert.equal(app.androidMaxVersion, '7.1.1'); | ||
}); | ||
}); | ||
}); |
@@ -67,7 +67,7 @@ 'use strict'; | ||
describe('more results mapping', () => { | ||
it('schould return few netflix apps', () => { | ||
it('should return few netflix apps', () => { | ||
return gplay.search({ term: 'netflix' }) | ||
.then((apps) => { | ||
assert.equal(apps[0].appId, 'com.netflix.mediaclient'); | ||
assertIdsInArray(apps, 'com.netflix.ninja', 'com.netflix.NGP.StrangerThings'); | ||
assert.isAbove(apps.length, 0); | ||
}); | ||
@@ -80,11 +80,12 @@ }); | ||
assert.equal(apps[0].appId, 'com.netflix.mediaclient'); | ||
assertIdsInArray(apps, 'com.netflix.ninja', 'com.netflix.NGP.StrangerThings'); | ||
// Don't check specific ids, as results may vary | ||
assert.isAbove(apps.length, 1); | ||
}); | ||
}); | ||
it('should reutrn few google mail apps', () => { | ||
it('should return few google mail apps', () => { | ||
return gplay.search({ term: 'gmail' }) | ||
.then((apps) => { | ||
assert.equal(apps[0].appId, 'com.google.android.gm'); | ||
assertIdsInArray(apps, 'com.google.android.gm.lite', 'com.google.android.apps.docs'); | ||
assert.isTrue(apps.some((app) => app.appId === 'com.google.android.gm.lite')); | ||
}); | ||
@@ -127,3 +128,3 @@ }); | ||
apps.map(assertValidApp); | ||
assertIdsInArray(apps, 'com.runtastic.android', 'running.tracker.gps.map', 'com.google.android.apps.fitness'); | ||
assertIdsInArray(apps, 'com.runtastic.android', 'running.tracker.gps.map'); | ||
}); | ||
@@ -130,0 +131,0 @@ }); |
@@ -14,5 +14,5 @@ 'use strict'; | ||
it('should fetch games from different developers', () => { | ||
return gplay.similar({ appId: 'com.mojang.minecraftpe' }) | ||
return gplay.similar({ appId: 'com.instagram.android' }) | ||
.then((apps) => assert.isTrue(apps.some(app => app.developer !== apps[0].developer))); | ||
}); | ||
}); |
const requestLib = require('got'); | ||
const throttled = require('../lib/utils/throttle'); | ||
const sinon = require('sinon'); | ||
const assert = require('chai').assert; | ||
it('Should make three requests with 5000ms interval. (Throttle function)', function (done) { | ||
this.timeout(15000); | ||
const req = throttled(requestLib, { | ||
limit: 1, | ||
interval: 5000 | ||
describe('Throttle tests', function () { | ||
this.timeout(6000); | ||
let server; | ||
// Create a fake http server to emulate http call and responses. | ||
before(function () { | ||
server = sinon.fakeServer.create(); | ||
}); | ||
Promise.all([req({ url: 'https://httpbin.org/uuid' }), req({ url: 'https://httpbin.org/uuid' }), req({ url: 'https://httpbin.org/uuid' })]) | ||
.then((response) => response.map(req => new Date(req.headers.date).getTime())) | ||
.then((dates) => { | ||
const firstAndSecondReq = dates[1] - dates[0]; | ||
const secondAndThirdReq = dates[2] - dates[1]; | ||
if ( | ||
(firstAndSecondReq >= 5000 && firstAndSecondReq <= 6500) && | ||
(secondAndThirdReq >= 5000 && secondAndThirdReq <= 6500) | ||
) { | ||
done(); | ||
} else { | ||
throw new Error('Wrong interval beetween requests.'); | ||
} | ||
// Remove any server responses added in current test suite. | ||
after(function () { | ||
server.restore(); | ||
}); | ||
const url = 'https://yesno.wtf/api'; // Fake url used in this test, it could be anything. | ||
it('Should make three requests with 2000ms interval. (Throttle function)', function () { | ||
// If we don't want to rely on the availability of a particular api we can use mocks. | ||
// The fake server intercept http calls and return specified objects if it mach the same method/url. | ||
server.respondWith('GET', url, JSON.stringify({ test: 'this works' })); | ||
const req = throttled(requestLib, { | ||
limit: 1, | ||
interval: 2000 | ||
}); | ||
return Promise.all([req({ url }), req({ url }), req({ url })]) | ||
.then((response) => response.map(req => new Date(req.headers.date).getTime())) | ||
.then((dates) => { | ||
const firstAndSecondReq = dates[1] - dates[0]; | ||
const secondAndThirdReq = dates[2] - dates[1]; | ||
assert.isAtLeast(firstAndSecondReq, 1000); | ||
assert.isAtMost(firstAndSecondReq, 3000); | ||
assert.isAtLeast(secondAndThirdReq, 1000); | ||
assert.isAtMost(secondAndThirdReq, 3000); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
144276
2.76%2924
3.43%628
1.29%12
9.09%+ Added
+ Added
- Removed
- Removed
Updated
Updated