NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Google Play Debug // @namespace https://github.com/catcto/google-play-debug // @description Google Play Tools, APK Downloader, Get APP Info, Dev Info... // @icon https://www.gstatic.com/android/market_images/web/favicon_v2.ico // @homepage https://github.com/catcto/google-play-debug // @supportURL https://github.com/catcto/google-play-debug/issues // @version 0.3 // @author catcto // @match https://play.google.com/store/* // @license MIT // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_openInTab // @grant GM_registerMenuCommand // @require https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js // @run-at document-end // ==/UserScript== (function () { const DETAILS_REGEX = /https\:\/\/play\.google\.com\/store\/apps\/details\?id=/i; const DEVELOPER_REGEX = /https:\/\/play\.google\.com\/store\/apps\/dev\?id=/i; const COLLECTION_REGEX = /https:\/\/play\.google\.com\/store\/apps\/collection/i; const MAPPINGS_DETAILS = { title: ['ds:5', 1, 2, 0, 0], description: { path: ['ds:5', 1, 2, 72, 0, 1], fun: descriptionText, }, descriptionHTML: ['ds:5', 1, 2, 72, 0, 1], summary: ['ds:5', 1, 2, 73, 0, 1], installs: ['ds:5', 1, 2, 13, 0], minInstalls: { path: ['ds:5', 1, 2, 13, 0], fun: cleanInt, }, score: ['ds:5', 1, 2, 51, 0, 1], scoreText: ['ds:5', 1, 2, 51, 0, 0], ratings: ['ds:5', 1, 2, 51, 2, 1], reviews: null, histogram: { path: ['ds:5', 1, 2, 51, 1], fun: buildHistogram, }, offersIAP: { path: ['ds:5', 1, 2, 19, 0], fun: Boolean, }, IAPRange: ['ds:5', 1, 2, 19, 0], size: null, androidVersion: ['ds:5', 1, 2, 140, 1, 1, 0, 0, 1], androidVersionText: ['ds:5', 1, 2, 140, 1, 1, 0, 0, 1], developer: ['ds:5', 1, 2, 68, 0], developerId: { path: ['ds:5', 1, 2, 68, 1, 4, 2], fun: (devUrl) => devUrl.split('id=')[1], }, developerEmail: ['ds:5', 1, 2, 69, 1, 0], developerWebsite: ['ds:5', 1, 2, 69, 0, 5, 2], developerAddress: ['ds:5', 1, 2, 69, 2, 0], privacyPolicy: ['ds:5', 1, 2, 99, 0, 5, 2], developerInternalID: { path: ['ds:5', 1, 2, 68, 1, 4, 2], fun: (devUrl) => devUrl.split('id=')[1], }, genre: ['ds:6', 1, 1, 0, 21, 0, 4, 5], genreId: ['ds:5', 1, 2, 79, 0, 0, 2], familyGenre: null, familyGenreId: null, icon: ['ds:5', 1, 2, 95, 0, 3, 2], headerImage: ['ds:5', 1, 2, 96, 0, 3, 2], screenshots: { path: ['ds:5', 1, 2, 78, 0], fun: R.map(R.path([3, 2])), }, video: ['ds:5', 1, 2, 100, 0, 0, 3, 2], videoImage: ['ds:5', 1, 2, 100, 1, 0, 3, 2], contentRating: ['ds:5', 1, 2, 9, 0], contentRatingDescription: ['ds:5', 1, 2, 9, 2, 1], adSupported: null, released: ['ds:5', 1, 2, 10, 0], updated: { path: ['ds:5', 1, 2, 145, 0, 0], fun: (date) => new Date(date).getTime(), }, version: ['ds:5', 1, 2, 140, 0, 0, 0], recentChanges: ['ds:5', 1, 2, 82, 1, 1], comments: null, pre_price: null, inAppProducts: ['ds:5', 1, 2, 19, 0], interactiveElements: { path: ['ds:5', 1, 2, 9, 3, 1], fun: getInteractiveElements, }, descriptionTranslation: null, descriptionShort: ['ds:5', 1, 2, 73, 0, 1], banner: ['ds:5', 1, 2, 96, 0, 3, 2], contentRatingArr: { path: ['ds:5', 1, 2, 9], fun: getContentRatingArr, }, developerPage: { path: ['ds:5', 1, 2, 68, 1, 4, 2], fun: developerPage, }, tags: { path: { ranking: ['ds:5', 1, 2, 58], genre: ['ds:5', 1, 2, 79], tag: ['ds:5', 1, 2, 118], }, fun: getApkTags, }, data_safety:{ path: ['ds:6', 1, 2, 136, 1], fun: getDataSafety, }, permissions:{ path: ['ds:5', 1,2,74,2], fun: getPermissions, } }; const MAPPING_SIMILAR = { path: ['ds:7', 1, 1, 0, 0, 3, 4, 2], }; const MAPPINGS_DEVELOPER = { name: ['ds:5', 0, 0, 0], banner: ['ds:5', 0, 9, 0, 3, 2], icon: ['ds:5', 0, 9, 1, 3, 2], website_url: ['ds:5', 0, 9, 2, 0, 5, 2], description: ['ds:5', 0, 10, 1, 1], }; const MAPPINGS_COLLECTION = { title: [2], appId: [12, 0], url: { path: [9, 4, 2], fun: (path) => ('https://play.google.com' + path) }, icon: [1, 1, 0, 3, 2], developer: [4, 0, 0, 0], developerId: { path: [4, 0, 0, 1, 4, 2], fun: extaractDeveloperId }, priceText: { path: [7, 0, 3, 2, 1, 0, 2], fun: (price) => price === undefined ? 'FREE' : price }, free: { path: [7, 0, 3, 2, 1, 0, 2], fun: (price) => price === undefined }, summary: [4, 1, 1, 1, 1], scoreText: [6, 0, 2, 1, 0], score: [6, 0, 2, 1, 1] }; const MAPPINGS_INIT = { apps: ['ds:3', 0, 1, 0, 0, 0], token: ['ds:3', 0, 1, 0, 0, 7, 1], categories: ['ds:3', 0, 1] }; function extaractDeveloperId (link) { return link.split('?id=')[1]; } function descriptionText(description) { return description.replace(/<br>/g, '\r\n'); } function cleanInt(number) { number = number || '0'; return parseInt(number); } function normalizeAndroidVersion(androidVersionText) { androidVersionText = androidVersionText || ''; const number = androidVersionText.split(' ')[0]; if (parseFloat(number)) { return number; } return 'VARY'; } function buildHistogram(container) { if (!container) { return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; } return { 1: container[1][1], 2: container[2][1], 3: container[3][1], 4: container[4][1], 5: container[5][1], }; } function getContentRatingArr(arrList) { var content2 = R.path([2, 1], arrList); var contentRating = [R.path([0], arrList)]; if (content2) { contentRating.push(content2); } return contentRating; } function priceText(priceText) { if (!priceText) { return 'Free'; } return priceText; } function extractComments(comments) { if (!comments) { return []; } return R.compose(R.take(40), R.reject(R.isNil), R.pluck(4))(comments); } function developerPage(devUrl) { if (devUrl.split('id=')[1] && Number(devUrl.split('id=')[1])) { return true; } return false; } function getContentRating(arrList) { var content2 = R.path([2, 1], arrList); var contentRating = [R.path([0], arrList)]; if (content2) { contentRating.push(content2); } return contentRating; } function getDataSafety(arrList) { if (!arrList) { return []; } var list = []; arrList.forEach(function (item) { list.push(item[1]); }); return list; } function getPermissions(arrList) { if (!arrList) { return []; } var permissions = { common:[], other:[], expand:[] }; if(arrList[0] && arrList[0].length > 0){ arrList[0].forEach(function (item) { if(item[2] && item[2].length > 0){ item[2].forEach(function (s) { permissions.common.push(s[1]); }); } }); } if(arrList[1] && arrList[1].length > 0 && arrList[1][0] && arrList[1][0].length > 0 && arrList[1][0][2] && arrList[1][0][2].length > 0){ arrList[1][0][2].forEach(function (item) { permissions.other.push(item[1]); }); } if(arrList[2] && arrList[2].length > 0){ arrList[2].forEach(function (item) { permissions.expand.push(item[1]); }); } return permissions; } function getInteractiveElements(interactiveElementText) { if (!interactiveElementText) { return []; } interactiveElementText = interactiveElementText || ''; var interactiveElementList = []; interactiveElementText.split(',').forEach(function (item) { item = item.trim(); interactiveElementList.push(item); }); return interactiveElementList; } function extractFields(parsedData, MAPPINGS) { return R.map((spec) => { if (!spec) return; if (R.is(Array, spec)) { return R.path(spec, parsedData); } if (R.is(Array, spec.path)) { // assume spec object const input = R.path(spec.path, parsedData); return spec.fun(input); } return spec.fun(spec.path, parsedData); }, MAPPINGS); } function matchScriptData(response) { const scriptRegex = />AF_initDataCallback[\s\S]*?<\/script/g; const keyRegex = /(ds:.*?)'/; const valueRegex = /data:([\s\S]*?), sideChannel: {}}\);<\//; return response.match(scriptRegex).reduce((accum, data) => { const keyMatch = data.match(keyRegex); const valueMatch = data.match(valueRegex); if (keyMatch && valueMatch) { const key = keyMatch[1]; const value = JSON.parse(valueMatch[1]); return R.assoc(key, value, accum); } return accum; }, {}); } function getTagName(data, result = []) { if (!data) return result; data.forEach(i => i && i.length === 4 && typeof i[0] === 'string' ? result.push(i[0]) : getTagName(i, result)); return result; } function saveTagName(type, tag, tagRecord, result) { if (tagRecord[tag]) return; tagRecord[tag] = true; result.push({ type, tag }); } function getApkTags(pathObj, parsedData) { const result = []; const tagRecord = {}; for (const [key, path] of Object.entries(pathObj)) { const data = R.path(path, parsedData); if (data === null || data.length === 0) continue; switch (key) { case 'ranking': { const tagName = `${data[2]} ${data[0]}`; saveTagName(key, tagName, tagRecord, result); break; } case 'genre': { const tagName = R.path([0, 0, 0], data); saveTagName(key, tagName, tagRecord, result); break; } case 'tag': { const tagNames = getTagName(data); tagNames.forEach(i => saveTagName(key, i, tagRecord, result)); break; } } } return result; } function details() { let params = new URLSearchParams(location.search); if (DETAILS_REGEX.test(location.href)) { console.log('app package name 🔍', params.get('id')); let parsedData = matchScriptData(document.body.innerHTML); console.log('app ds 🔍', JSON.stringify(parsedData)); let appData = extractFields(parsedData, MAPPINGS_DETAILS); console.log('app parsed 🔍', JSON.stringify(appData, null, 4)); let similarData = extractFields(parsedData, MAPPING_SIMILAR); console.log('app similar url 🔍', similarData); } } function developer() { let params = new URLSearchParams(location.search); if (DEVELOPER_REGEX.test(location.href)) { console.log('developer id 🔍', params.get('id')); let parsedData = matchScriptData(document.body.innerHTML); let devData = extractFields(parsedData, MAPPINGS_DEVELOPER); console.log('developer ds 🔍', JSON.stringify(parsedData)); console.log('developer parsed 🔍', JSON.stringify(devData)); } } function collection() { if (COLLECTION_REGEX.test(location.href)) { let parsedData = matchScriptData(document.body.innerHTML); let appsData = extractFields(parsedData, MAPPINGS_INIT); console.log('list apps ds 🔍', JSON.stringify(appsData.apps)); if(appsData.apps.length > 0){ let listData = []; appsData.apps.forEach(function(item){ listData.push(extractFields(item, MAPPINGS_COLLECTION)); }); console.log('list parsed 🔍', JSON.stringify(listData)); } } } function collection_html(){ if (COLLECTION_REGEX.test(location.href)) { let listData = []; document.querySelectorAll('.Vpfmgd').forEach(function(el){ listData.push({ title:el.querySelector('.WsMG1c.nnK0zc').innerText }); }) console.log('list parsed 🔍', JSON.stringify(listData)); } } function init() { GM_registerMenuCommand("1. details parser", details); GM_registerMenuCommand("2. developer parser", developer); GM_registerMenuCommand("3. collection parser", collection_html); } init(); })();