mirror of
https://github.com/FliegendeWurst/googleplay-api.git
synced 2024-11-21 20:44:56 +00:00
Updated Protobuf definition and parsing
Newer definition have been fetched from play-store-api. Moreover, the following enhanchements are introduced: - An new function utils.parseProtobufObj will automatically parse any Protobuf object into a dictionary, so there's no need to manually specify each field to parse - Added toc() and acceptTos() functions - Added deviceCheckinConsistencyToken and dfeCookie to API state and headers - Since search results are now divided into clusters, each one representing a different subcategory, now the search() API call returns a list of clusters, each one containing a list of apps
This commit is contained in:
parent
86e069b541
commit
46b427124d
202
googleplay.proto
202
googleplay.proto
@ -1,8 +1,9 @@
|
||||
syntax = "proto2";
|
||||
|
||||
// Both sha1 and sha256 are encoded with base64 with URL and Filename Safe Alphabet with padding removed
|
||||
message AndroidAppDeliveryData {
|
||||
optional int64 downloadSize = 1;
|
||||
optional string signature = 2;
|
||||
optional string sha1 = 2;
|
||||
optional string downloadUrl = 3;
|
||||
repeated AppFileMetadata additionalFile = 4;
|
||||
repeated HttpCookie downloadAuthCookie = 5;
|
||||
@ -13,10 +14,23 @@ message AndroidAppDeliveryData {
|
||||
optional bool immediateStartNeeded = 10;
|
||||
optional AndroidAppPatchData patchData = 11;
|
||||
optional EncryptionParams encryptionParams = 12;
|
||||
optional string downloadUrlGzipped = 13;
|
||||
optional int64 downloadSizeGzipped = 14;
|
||||
repeated Split split = 15;
|
||||
optional string sha256 = 19;
|
||||
}
|
||||
message Split {
|
||||
optional string name = 1;
|
||||
optional int64 size = 2;
|
||||
optional int64 sizeGzipped = 3;
|
||||
optional string sha1 = 4;
|
||||
optional string downloadUrl = 5;
|
||||
optional string downloadUrlGzipped = 6;
|
||||
optional string sha256 = 9;
|
||||
}
|
||||
message AndroidAppPatchData {
|
||||
optional int32 baseVersionCode = 1;
|
||||
optional string baseSignature = 2;
|
||||
optional string baseSha1 = 2;
|
||||
optional string downloadUrl = 3;
|
||||
optional int32 patchFormat = 4;
|
||||
optional int64 maxPatchSize = 5;
|
||||
@ -26,6 +40,9 @@ message AppFileMetadata {
|
||||
optional int32 versionCode = 2;
|
||||
optional int64 size = 3;
|
||||
optional string downloadUrl = 4;
|
||||
optional int64 sizeGzipped = 6;
|
||||
optional string downloadUrlGzipped = 7;
|
||||
optional string sha1 = 8;
|
||||
}
|
||||
message EncryptionParams {
|
||||
optional int32 version = 1;
|
||||
@ -232,7 +249,10 @@ message Offer {
|
||||
optional SubscriptionTerms subscriptionTerms = 12;
|
||||
optional string formattedName = 13;
|
||||
optional string formattedDescription = 14;
|
||||
optional string saleEnds = 31;
|
||||
optional bool sale = 22;
|
||||
optional string message = 26;
|
||||
optional int64 saleEndTimestamp = 30;
|
||||
optional string saleMessage = 31;
|
||||
}
|
||||
message OwnershipInfo {
|
||||
optional int64 initiationTimestampMsec = 1;
|
||||
@ -353,11 +373,34 @@ message DetailsResponse {
|
||||
optional DocV2 docV2 = 4;
|
||||
optional string footerHtml = 5;
|
||||
repeated Badge badge = 7;
|
||||
optional Features features = 12;
|
||||
optional string detailsStreamUrl = 13;
|
||||
optional string userReviewUrl = 14;
|
||||
optional string postAcquireDetailsStreamUrl = 17;
|
||||
}
|
||||
message Badge {
|
||||
optional string label = 1;
|
||||
optional Image image = 2;
|
||||
optional BadgeContainer1 badgeContainer1 = 4;
|
||||
optional string message = 11;
|
||||
}
|
||||
message BadgeContainer1 {
|
||||
optional BadgeContainer2 badgeContainer2 = 1;
|
||||
}
|
||||
message BadgeContainer2 {
|
||||
optional BadgeLinkContainer badgeLinkContainer = 2;
|
||||
}
|
||||
message BadgeLinkContainer {
|
||||
optional string link = 2;
|
||||
}
|
||||
message Features {
|
||||
repeated Feature featurePresence = 1;
|
||||
repeated Feature featureRating = 2;
|
||||
}
|
||||
message Feature {
|
||||
optional string label = 1;
|
||||
optional string value = 3;
|
||||
}
|
||||
message DeviceConfigurationProto {
|
||||
optional int32 touchScreen = 1;
|
||||
optional int32 keyboard = 2;
|
||||
@ -425,6 +468,7 @@ message Image {
|
||||
optional string url = 12;
|
||||
}
|
||||
optional string color = 15;
|
||||
optional int32 screenshotSetNumber = 21;
|
||||
}
|
||||
message TranslatedText {
|
||||
optional string text = 1;
|
||||
@ -467,10 +511,13 @@ message AppDetails {
|
||||
repeated FileMetadata file = 17;
|
||||
optional string appType = 18;
|
||||
optional bool unstable = 21;
|
||||
optional bool hasInstantLink = 24;
|
||||
optional string containsAds = 30;
|
||||
optional Dependencies dependencies = 34;
|
||||
optional TestingProgramInfo testingProgramInfo = 35;
|
||||
optional EarlyAccessInfo earlyAccessInfo = 36;
|
||||
optional string instantLink = 43;
|
||||
optional string developerAddress = 45;
|
||||
}
|
||||
message Dependencies {
|
||||
optional int32 unknown1 = 1;
|
||||
@ -610,7 +657,7 @@ message Bucket {
|
||||
}
|
||||
message ListResponse {
|
||||
repeated Bucket bucket = 1;
|
||||
repeated SearchCluster cluster = 2;
|
||||
repeated DocV2 doc = 2;
|
||||
}
|
||||
message DocV1 {
|
||||
optional Document finskyDoc = 1;
|
||||
@ -655,7 +702,11 @@ message DocV2 {
|
||||
optional string purchaseDetailsUrl = 20;
|
||||
optional bool detailsReusable = 21;
|
||||
optional string subtitle = 22;
|
||||
optional UnknownCategoryContainer unknownCategoryContainer = 24;
|
||||
optional Unknown25 unknown25 = 25;
|
||||
optional string descriptionShort = 27;
|
||||
optional string reviewSnippetsUrl = 31;
|
||||
optional string reviewQuestionsUrl = 34;
|
||||
}
|
||||
message Unknown25 {
|
||||
repeated Unknown25Item item = 2;
|
||||
@ -679,6 +730,7 @@ message RelatedLinksUnknown1 {
|
||||
optional RelatedLinksUnknown2 unknown2 = 2;
|
||||
}
|
||||
message RelatedLinksUnknown2 {
|
||||
optional string homeUrl = 2;
|
||||
optional string nextPageUrl = 3;
|
||||
}
|
||||
message Rated {
|
||||
@ -830,19 +882,74 @@ message AggregateRating {
|
||||
optional uint64 commentCount = 11;
|
||||
optional double bayesianMeanRating = 12;
|
||||
}
|
||||
|
||||
message AcceptTosResponse {
|
||||
}message CarrierBillingConfig {
|
||||
optional string id = 1;
|
||||
optional string name = 2;
|
||||
optional int32 apiVersion = 3;
|
||||
optional string provisioningUrl = 4;
|
||||
optional string credentialsUrl = 5;
|
||||
optional bool tosRequired = 6;
|
||||
optional bool perTransactionCredentialsRequired = 7;
|
||||
optional bool sendSubscriberIdWithCarrierBillingRequests = 8;
|
||||
}
|
||||
message BillingConfig {
|
||||
optional CarrierBillingConfig carrierBillingConfig = 1;
|
||||
optional int32 maxIabApiVersion = 2;
|
||||
}
|
||||
message CorpusMetadata {
|
||||
optional int32 backend = 1;
|
||||
optional string name = 2;
|
||||
optional string landingUrl = 3;
|
||||
optional string libraryName = 4;
|
||||
optional string recsWidgetUrl = 6;
|
||||
optional string shopName = 7;
|
||||
}
|
||||
message Experiments {
|
||||
repeated string experimentId = 1;
|
||||
}
|
||||
message SelfUpdateConfig {
|
||||
optional int32 latestClientVersionCode = 1;
|
||||
}
|
||||
message TocResponse {
|
||||
repeated CorpusMetadata corpus = 1;
|
||||
optional int32 tosVersionDeprecated = 2;
|
||||
optional string tosContent = 3;
|
||||
optional string homeUrl = 4;
|
||||
optional Experiments experiments = 5;
|
||||
optional string tosCheckboxTextMarketingEmails = 6;
|
||||
optional string tosToken = 7;
|
||||
optional string iconOverrideUrl = 9;
|
||||
optional SelfUpdateConfig selfUpdateConfig = 10;
|
||||
optional bool requiresUploadDeviceConfig = 11;
|
||||
optional BillingConfig billingConfig = 12;
|
||||
optional string recsWidgetUrl = 13;
|
||||
optional string socialHomeUrl = 15;
|
||||
optional bool ageVerificationRequired = 16;
|
||||
optional bool gplusSignupEnabled = 17;
|
||||
optional bool redeemEnabled = 18;
|
||||
optional string helpUrl = 19;
|
||||
optional int32 themeId = 20;
|
||||
optional string entertainmentHomeUrl = 21;
|
||||
optional string cookie = 22;
|
||||
}
|
||||
message Payload {
|
||||
optional ListResponse listResponse = 1;
|
||||
optional DetailsResponse detailsResponse = 2;
|
||||
optional ReviewResponse reviewResponse = 3;
|
||||
optional BuyResponse buyResponse = 4;
|
||||
optional SearchResponse searchResponse = 5;
|
||||
optional TocResponse tocResponse = 6;
|
||||
optional BrowseResponse browseResponse = 7;
|
||||
optional PurchaseStatusResponse purchaseStatusResponse = 8;
|
||||
optional string logResponse = 10;
|
||||
optional string flagContentResponse = 13;
|
||||
optional BulkDetailsResponse bulkDetailsResponse = 19;
|
||||
optional DeliveryResponse deliveryResponse = 21;
|
||||
optional UploadDeviceConfigResponse uploadDeviceConfigResponse = 25;
|
||||
optional AcceptTosResponse acceptTosResponse = 22;
|
||||
optional AndroidCheckinResponse androidCheckinResponse = 26;
|
||||
optional UploadDeviceConfigResponse uploadDeviceConfigResponse = 28;
|
||||
optional SearchSuggestResponse searchSuggestResponse = 40;
|
||||
optional TestingProgramResponse testingProgramResponse = 80;
|
||||
}
|
||||
@ -853,11 +960,40 @@ message PreFetch {
|
||||
optional int64 ttl = 4;
|
||||
optional int64 softTtl = 5;
|
||||
}
|
||||
message ServerMetadata {
|
||||
optional int64 latencyMillis = 1;
|
||||
}
|
||||
|
||||
message Targets {
|
||||
repeated int64 targetId = 1;
|
||||
optional bytes signature = 2;
|
||||
}
|
||||
|
||||
message ServerCookie {
|
||||
optional int32 type = 1;
|
||||
optional bytes token = 2;
|
||||
}
|
||||
message ServerCookies {
|
||||
repeated ServerCookie serverCookie = 1;
|
||||
}
|
||||
message ResponseWrapper {
|
||||
optional Payload payload = 1;
|
||||
optional ServerCommands commands = 2;
|
||||
repeated PreFetch preFetch = 3;
|
||||
repeated Notification notification = 4;
|
||||
optional ServerMetadata serverMetadata = 5;
|
||||
optional Targets targets = 6;
|
||||
optional ServerCookies serverCookies = 7;
|
||||
optional bytes serverLogsCookie = 9;
|
||||
}
|
||||
message ResponseWrapperApi {
|
||||
optional PayloadApi payload = 1;
|
||||
}
|
||||
message PayloadApi {
|
||||
optional UserProfileResponse userProfileResponse = 5;
|
||||
}
|
||||
message UserProfileResponse {
|
||||
optional UserProfile userProfile = 1;
|
||||
}
|
||||
message ServerCommands {
|
||||
optional bool clearCache = 1;
|
||||
@ -881,30 +1017,23 @@ message Review {
|
||||
optional string deviceName = 19;
|
||||
optional string replyText = 29;
|
||||
optional int64 replyTimestampMsec = 30;
|
||||
optional Author author = 31;
|
||||
optional Author2 author2 = 33;
|
||||
optional ReviewAuthor author = 31;
|
||||
optional UserProfile userProfile = 33;
|
||||
}
|
||||
message Author {
|
||||
message ReviewAuthor {
|
||||
optional string name = 2;
|
||||
optional Avatar urls = 5;
|
||||
optional Image avatar = 5;
|
||||
}
|
||||
message Author2 {
|
||||
message UserProfile {
|
||||
optional string personIdString = 1;
|
||||
optional string personId = 2;
|
||||
optional int32 unknown1 = 3;
|
||||
optional int32 unknown2 = 4;
|
||||
optional string name = 5;
|
||||
optional Avatar urls = 10;
|
||||
repeated Image image = 10;
|
||||
optional string googlePlusUrl = 19;
|
||||
optional string googlePlusTagline = 22;
|
||||
}
|
||||
message Avatar {
|
||||
optional int32 unknown1 = 1;
|
||||
optional string url = 5;
|
||||
optional string secureUrl = 7;
|
||||
optional bool unknown2 = 9;
|
||||
optional bool unknown3 = 22;
|
||||
}
|
||||
message ReviewResponse {
|
||||
optional GetReviewsResponse getResponse = 1;
|
||||
optional string nextPageUrl = 2;
|
||||
@ -927,18 +1056,6 @@ message SearchResponse {
|
||||
repeated RelatedSearch relatedSearch = 6;
|
||||
optional string nextPageUrl = 10;
|
||||
}
|
||||
message UserProfileResponseWrapper {
|
||||
optional UserProfilePayload payload = 1;
|
||||
optional ServerCommands commands = 2;
|
||||
repeated PreFetch preFetch = 3;
|
||||
repeated Notification notification = 4;
|
||||
}
|
||||
message UserProfilePayload {
|
||||
optional UserProfileResponse response = 5;
|
||||
}
|
||||
message UserProfileResponse {
|
||||
repeated DocV2 doc = 1;
|
||||
}
|
||||
message SearchSuggestResponse {
|
||||
repeated SearchSuggestEntry entry = 1;
|
||||
}
|
||||
@ -1018,6 +1135,7 @@ message AndroidCheckinResponse {
|
||||
optional fixed64 securityToken = 8;
|
||||
optional bool settingsDiff = 9;
|
||||
repeated string deleteSetting = 10;
|
||||
optional string deviceCheckinConsistencyToken = 12;
|
||||
}
|
||||
message GservicesSetting {
|
||||
optional bytes name = 1;
|
||||
@ -1139,11 +1257,21 @@ message StatCounters {
|
||||
message UsageStatsExtensionProto {
|
||||
optional AndroidDataUsageProto dataUsage = 1;
|
||||
}
|
||||
message SearchCluster {
|
||||
optional string id = 1;
|
||||
optional string type = 2;
|
||||
optional int64 int1 = 3;
|
||||
optional int64 int2 = 4;
|
||||
optional string category = 5;
|
||||
repeated DocV2 doc = 11;
|
||||
message ModifyLibraryRequest {
|
||||
optional string libraryId = 1;
|
||||
repeated string addPackageName = 2;
|
||||
repeated string removePackageName = 3;
|
||||
}
|
||||
message UrlRequestWrapper {
|
||||
optional DeveloperAppsRequest developerAppsRequest = 49;
|
||||
}
|
||||
message DeveloperAppsRequest {
|
||||
optional DeveloperIdContainer developerIdContainer1 = 1;
|
||||
optional DeveloperIdContainer developerIdContainer2 = 2;
|
||||
optional int32 unknownInt3 = 3;
|
||||
}
|
||||
message DeveloperIdContainer {
|
||||
optional string developerId = 1;
|
||||
optional int32 unknownInt2 = 2;
|
||||
optional int32 unknownInt3 = 3;
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ SEARCH_SUGGEST_URL = FDFE + "searchSuggest"
|
||||
BULK_URL = FDFE + "bulkDetails"
|
||||
LOG_URL = FDFE + "log"
|
||||
TOC_URL = FDFE + "toc"
|
||||
ACCEPT_TOS_URL = FDFE + "acceptTos"
|
||||
LIST_URL = FDFE + "list"
|
||||
REVIEWS_URL = FDFE + "rev"
|
||||
|
||||
@ -72,6 +73,8 @@ class GooglePlayAPI(object):
|
||||
self.authSubToken = None
|
||||
self.gsfId = None
|
||||
self.device_config_token = None
|
||||
self.deviceCheckinConsistencyToken = None
|
||||
self.dfeCookie = None
|
||||
self.proxies_config = proxies_config
|
||||
self.deviceBuilder = config.DeviceBuilder(device_codename)
|
||||
self.set_locale(locale)
|
||||
@ -141,6 +144,10 @@ class GooglePlayAPI(object):
|
||||
headers["Authorization"] = "GoogleLogin auth=%s" % self.authSubToken
|
||||
if self.device_config_token is not None:
|
||||
headers["X-DFE-Device-Config-Token"] = self.device_config_token
|
||||
if self.deviceCheckinConsistencyToken is not None:
|
||||
headers["X-DFE-Device-Checkin-Consistency-Token"] = self.deviceCheckinConsistencyToken
|
||||
if self.dfeCookie is not None:
|
||||
headers["X-DFE-Cookie"] = self.dfeCookie
|
||||
return headers
|
||||
|
||||
def checkin(self, email, ac2dmToken):
|
||||
@ -155,6 +162,7 @@ class GooglePlayAPI(object):
|
||||
proxies=self.proxies_config)
|
||||
response = googleplay_pb2.AndroidCheckinResponse()
|
||||
response.ParseFromString(res.content)
|
||||
self.deviceCheckinConsistencyToken = response.deviceCheckinConsistencyToken
|
||||
|
||||
# checkin again to upload gfsid
|
||||
request.id = response.androidId
|
||||
@ -226,7 +234,7 @@ class GooglePlayAPI(object):
|
||||
if "NeedsBrowser" in params["error"]:
|
||||
raise SecurityCheckError("Security check is needed, try to visit "
|
||||
"https://accounts.google.com/b/0/DisplayUnlockCaptcha "
|
||||
"to unlock, or setup an app-specific passwor")
|
||||
"to unlock, or setup an app-specific password")
|
||||
raise LoginError("server says: " + params["error"])
|
||||
else:
|
||||
raise LoginError("Auth token not found.")
|
||||
@ -239,7 +247,7 @@ class GooglePlayAPI(object):
|
||||
self.gsfId = gsfId
|
||||
self.setAuthSubToken(authSubToken)
|
||||
# check if token is valid with a simple search
|
||||
self.search('firefox', 1, None)
|
||||
self.search('drv', 1, None)
|
||||
else:
|
||||
raise LoginError('Either (email,pass) or (gsfId, authSubToken) is needed')
|
||||
|
||||
@ -335,10 +343,10 @@ class GooglePlayAPI(object):
|
||||
"ssis": "120",
|
||||
"sst": "2"}
|
||||
data = self.executeRequestApi2(SEARCH_SUGGEST_URL, params=params)
|
||||
response = data.payload.searchSuggestResponse
|
||||
return [{"type": e.type,
|
||||
"suggestedQuery": e.suggestedQuery,
|
||||
"title": e.title} for e in response.entry]
|
||||
output = []
|
||||
for entry in data.payload.searchSuggestResponse.entry:
|
||||
output.append(utils.parseProtobufObj(entry))
|
||||
return output
|
||||
|
||||
def search(self, query, nb_result, offset=None):
|
||||
""" Search the play store for an app.
|
||||
@ -347,49 +355,26 @@ class GooglePlayAPI(object):
|
||||
|
||||
offset is used to take result starting from an index.
|
||||
"""
|
||||
# TODO: correctly implement nb_results (for now it does nothing)
|
||||
if self.authSubToken is None:
|
||||
raise Exception("You need to login before executing any request")
|
||||
|
||||
remaining = nb_result
|
||||
output = []
|
||||
|
||||
nextPath = SEARCH_URL + "?c=3&q={}".format(requests.utils.quote(query))
|
||||
path = SEARCH_URL + "?c=3&q={}".format(requests.utils.quote(query))
|
||||
if (offset is not None):
|
||||
nextPath += "&o={}".format(offset)
|
||||
while remaining > 0 and nextPath is not None:
|
||||
currentPath = nextPath
|
||||
data = self.executeRequestApi2(currentPath)
|
||||
if utils.hasPrefetch(data):
|
||||
response = data.preFetch[0].response
|
||||
else:
|
||||
response = data
|
||||
if utils.hasSearchResponse(response.payload):
|
||||
# we still need to fetch the first page, so go to
|
||||
# next loop iteration without decrementing counter
|
||||
nextPath = FDFE + response.payload.searchResponse.nextPageUrl
|
||||
continue
|
||||
if utils.hasListResponse(response.payload):
|
||||
cluster = response.payload.listResponse.cluster
|
||||
if len(cluster) == 0:
|
||||
# unexpected behaviour, probably due to expired token
|
||||
raise LoginError('Unexpected behaviour, probably expired '
|
||||
'token')
|
||||
cluster = cluster[0]
|
||||
if len(cluster.doc) == 0:
|
||||
break
|
||||
if cluster.doc[0].containerMetadata.nextPageUrl != "":
|
||||
nextPath = FDFE + cluster.doc[0].containerMetadata.nextPageUrl
|
||||
else:
|
||||
nextPath = None
|
||||
apps = []
|
||||
for doc in cluster.doc:
|
||||
apps.extend(doc.child)
|
||||
output += list(map(utils.fromDocToDictionary, apps))
|
||||
remaining -= len(apps)
|
||||
|
||||
if len(output) > nb_result:
|
||||
output = output[:nb_result]
|
||||
|
||||
# TODO: not sure if this toc call should be here
|
||||
self.toc()
|
||||
data = self.executeRequestApi2(path)
|
||||
if utils.hasPrefetch(data):
|
||||
response = data.preFetch[0].response
|
||||
else:
|
||||
response = data
|
||||
output = []
|
||||
for cluster in response.payload.listResponse.doc[0].child:
|
||||
output.append(utils.parseProtobufObj(cluster))
|
||||
return output
|
||||
|
||||
def details(self, packageName):
|
||||
@ -398,7 +383,7 @@ class GooglePlayAPI(object):
|
||||
packageName is the app unique ID (usually starting with 'com.')."""
|
||||
path = DETAILS_URL + "?doc={}".format(requests.utils.quote(packageName))
|
||||
data = self.executeRequestApi2(path)
|
||||
return utils.fromDocToDictionary(data.payload.detailsResponse.docV2)
|
||||
return utils.parseProtobufObj(data.payload.detailsResponse.docV2)
|
||||
|
||||
def bulkDetails(self, packageNames):
|
||||
"""Get several apps details from a list of package names.
|
||||
@ -424,18 +409,19 @@ class GooglePlayAPI(object):
|
||||
params=params)
|
||||
response = message.payload.bulkDetailsResponse
|
||||
return [None if not utils.hasDoc(entry) else
|
||||
utils.fromDocToDictionary(entry.doc)
|
||||
utils.parseProtobufObj(entry.doc)
|
||||
for entry in response.entry]
|
||||
|
||||
def getHomeApps(self):
|
||||
path = HOME_URL + "?c=3&nocache_isui=true"
|
||||
data = self.executeRequestApi2(path)
|
||||
if utils.hasPrefetch(data):
|
||||
response = data.preFetch[0].response
|
||||
else:
|
||||
response = data
|
||||
output = []
|
||||
cluster = data.preFetch[0].response.payload.listResponse.cluster[0]
|
||||
for doc in cluster.doc:
|
||||
output.append({"categoryId": doc.docid,
|
||||
"categoryStr": doc.title,
|
||||
"apps": [utils.fromDocToDictionary(c) for c in doc.child]})
|
||||
for cluster in response.payload.listResponse.doc[0].child:
|
||||
output.append(utils.parseProtobufObj(cluster))
|
||||
return output
|
||||
|
||||
def browse(self, cat=None, subCat=None):
|
||||
@ -451,29 +437,26 @@ class GooglePlayAPI(object):
|
||||
|
||||
if cat is None and subCat is None:
|
||||
# result contains all categories available
|
||||
return [{'name': c.name,
|
||||
'dataUrl': c.dataUrl,
|
||||
'catId': c.unknownCategoryContainer.categoryIdContainer.categoryId}
|
||||
for c in data.payload.browseResponse.category]
|
||||
return [utils.parseProtobufObj(c) for c in data.payload.browseResponse.category]
|
||||
|
||||
output = []
|
||||
clusters = []
|
||||
|
||||
if utils.hasPrefetch(data):
|
||||
for pf in data.preFetch:
|
||||
clusters.extend(pf.response.payload.listResponse.cluster)
|
||||
for cluster in pf.response.payload.listResponse.doc:
|
||||
clusters.extend(cluster.child)
|
||||
|
||||
# result contains apps of a specific category
|
||||
# organized by sections
|
||||
for cluster in clusters:
|
||||
for doc in cluster.doc:
|
||||
apps = [a for a in doc.child]
|
||||
apps = list(map(utils.fromDocToDictionary,
|
||||
apps))
|
||||
section = {'title': doc.title,
|
||||
'docid': doc.docid,
|
||||
'apps': apps}
|
||||
output.append(section)
|
||||
apps = [a for a in cluster.child]
|
||||
apps = list(map(utils.parseProtobufObj,
|
||||
apps))
|
||||
section = {'title': cluster.title,
|
||||
'docid': cluster.docid,
|
||||
'apps': apps}
|
||||
output.append(section)
|
||||
return output
|
||||
|
||||
def list(self, cat, ctr=None, nb_results=None, offset=None):
|
||||
@ -495,19 +478,16 @@ class GooglePlayAPI(object):
|
||||
if ctr is None:
|
||||
# list subcategories
|
||||
for pf in data.preFetch:
|
||||
clusters.extend(pf.response.payload.listResponse.cluster)
|
||||
for c in clusters:
|
||||
docs.extend(c.doc)
|
||||
return [d.docid for d in docs]
|
||||
for cluster in pf.response.payload.listResponse.doc:
|
||||
clusters.extend(cluster.child)
|
||||
return [c.docid for c in clusters]
|
||||
else:
|
||||
childs = []
|
||||
clusters.extend(data.payload.listResponse.cluster)
|
||||
for c in clusters:
|
||||
docs.extend(c.doc)
|
||||
for d in docs:
|
||||
childs.extend(d.child)
|
||||
return [utils.fromDocToDictionary(c)
|
||||
for c in childs]
|
||||
apps = []
|
||||
for d in data.payload.listResponse.doc:
|
||||
for c in d.child: # category
|
||||
for a in c.child: # app
|
||||
apps.append(utils.parseProtobufObj(a))
|
||||
return apps
|
||||
|
||||
def reviews(self, packageName, filterByDevice=False, sort=2,
|
||||
nb_results=None, offset=None):
|
||||
@ -524,6 +504,7 @@ class GooglePlayAPI(object):
|
||||
dict object containing all the protobuf data returned from
|
||||
the api
|
||||
"""
|
||||
# TODO: select the number of reviews to return
|
||||
path = REVIEWS_URL + "?doc={}&sort={}".format(requests.utils.quote(packageName), sort)
|
||||
if nb_results is not None:
|
||||
path += "&n={}".format(nb_results)
|
||||
@ -533,19 +514,8 @@ class GooglePlayAPI(object):
|
||||
path += "&dfil=1"
|
||||
data = self.executeRequestApi2(path)
|
||||
output = []
|
||||
for rev in data.payload.reviewResponse.getResponse.review:
|
||||
author = {'personIdString': rev.author2.personIdString,
|
||||
'personId': rev.author2.personId,
|
||||
'name': rev.author2.name,
|
||||
'profilePicUrl': rev.author2.urls.url,
|
||||
'googlePlusUrl': rev.author2.googlePlusUrl}
|
||||
review = {'documentVersion': rev.documentVersion,
|
||||
'timestampMsec': rev.timestampMsec,
|
||||
'starRating': rev.starRating,
|
||||
'comment': rev.comment,
|
||||
'commentId': rev.commentId,
|
||||
'author': author}
|
||||
output.append(review)
|
||||
for review in data.payload.reviewResponse.getResponse.review:
|
||||
output.append(utils.parseProtobufObj(review))
|
||||
return output
|
||||
|
||||
def _deliver_data(self, url, cookies):
|
||||
@ -650,7 +620,8 @@ class GooglePlayAPI(object):
|
||||
|
||||
if versionCode is None:
|
||||
# pick up latest version
|
||||
versionCode = self.details(packageName).get('versionCode')
|
||||
appDetails = self.details(packageName).get('details').get('appDetails')
|
||||
versionCode = appDetails.get('versionCode')
|
||||
|
||||
headers = self.getHeaders()
|
||||
params = {'ot': str(offerType),
|
||||
@ -687,6 +658,35 @@ class GooglePlayAPI(object):
|
||||
if response.commands.displayErrorMessage != "":
|
||||
raise RequestError(response.commands.displayErrorMessage)
|
||||
|
||||
def toc(self):
|
||||
response = requests.get(TOC_URL,
|
||||
headers=self.getHeaders(),
|
||||
verify=ssl_verify,
|
||||
timeout=60,
|
||||
proxies=self.proxies_config)
|
||||
data = googleplay_pb2.ResponseWrapper.FromString(response.content)
|
||||
tocResponse = data.payload.tocResponse
|
||||
if utils.hasTosContent(tocResponse) and utils.hasTosToken(tocResponse):
|
||||
self.acceptTos(tocResponse.tosToken)
|
||||
if utils.hasCookie(tocResponse):
|
||||
self.dfeCookie = tocResponse.cookie
|
||||
return utils.parseProtobufObj(tocResponse)
|
||||
|
||||
|
||||
def acceptTos(self, tosToken):
|
||||
params = {
|
||||
"tost": tosToken,
|
||||
"toscme": "false"
|
||||
}
|
||||
response = requests.get(ACCEPT_TOS_URL,
|
||||
headers=self.getHeaders(),
|
||||
params=params,
|
||||
verify=ssl_verify,
|
||||
timeout=60,
|
||||
proxies=self.proxies_config)
|
||||
data = googleplay_pb2.ResponseWrapper.FromString(response.content)
|
||||
return utils.parseProtobufObj(data.payload.acceptTosResponse)
|
||||
|
||||
@staticmethod
|
||||
def getDevicesCodenames():
|
||||
return config.getDevicesCodenames()
|
||||
|
File diff suppressed because one or more lines are too long
100
gpapi/utils.py
100
gpapi/utils.py
@ -1,60 +1,36 @@
|
||||
import struct
|
||||
import sys
|
||||
from google.protobuf.message import Message
|
||||
from . import googleplay_pb2
|
||||
|
||||
VERSION = sys.version_info[0]
|
||||
|
||||
def fromDocToDictionary(app):
|
||||
return {"docId": app.docid,
|
||||
"title": app.title,
|
||||
"author": app.creator,
|
||||
"description": app.descriptionHtml,
|
||||
"recentChanges": app.details.appDetails.recentChangesHtml,
|
||||
"offer": [{"micros": o.micros,
|
||||
"currencyCode": o.currencyCode,
|
||||
"formattedAmount": o.formattedAmount,
|
||||
"checkoutFlowRequired": o.checkoutFlowRequired,
|
||||
"offerType": o.offerType,
|
||||
"saleEnds": o.saleEnds}
|
||||
for o in app.offer],
|
||||
"images": [{"imageType": img.imageType,
|
||||
"width": img.dimension.width
|
||||
if hasattr(img.dimension, "width")
|
||||
else 0,
|
||||
"height": img.dimension.height
|
||||
if hasattr(img.dimension, "height")
|
||||
else 0,
|
||||
"url": img.imageUrl,
|
||||
"supportsFifeUrlOptions": img.supportsFifeUrlOptions}
|
||||
for img in app.image],
|
||||
"versionCode": app.details.appDetails.versionCode,
|
||||
"versionString": app.details.appDetails.versionString,
|
||||
"installationSize": app.details.appDetails.installationSize,
|
||||
"numDownloads": app.details.appDetails.numDownloads,
|
||||
"uploadDate": app.details.appDetails.uploadDate,
|
||||
"permission": [p for p in app.details.appDetails.permission],
|
||||
"files": [{"fileType": f.fileType,
|
||||
"version": f.versionCode,
|
||||
"size": f.size}
|
||||
for f in app.details.appDetails.file],
|
||||
"unstable": app.details.appDetails.unstable,
|
||||
"containsAds": app.details.appDetails.containsAds,
|
||||
"aggregateRating": {"type": app.aggregateRating.type,
|
||||
"starRating": app.aggregateRating.starRating,
|
||||
"ratingsCount": app.aggregateRating.ratingsCount,
|
||||
"oneStarRatings": app.aggregateRating.oneStarRatings,
|
||||
"twoStarRatings": app.aggregateRating.twoStarRatings,
|
||||
"threeStarRatings": app.aggregateRating.threeStarRatings,
|
||||
"fourStarRatings": app.aggregateRating.fourStarRatings,
|
||||
"fiveStarRatings": app.aggregateRating.fiveStarRatings,
|
||||
"commentCount": app.aggregateRating.commentCount},
|
||||
"dependencies": [{"packageName": d.packageName,
|
||||
"version": d.version}
|
||||
for d in app.details.appDetails.dependencies.dependency],
|
||||
"category": {"appType": app.relatedLinks.categoryInfo.appType,
|
||||
"appCategory": app.relatedLinks.categoryInfo.appCategory},
|
||||
"detailsUrl": app.detailsUrl}
|
||||
def isIterable(obj):
|
||||
try:
|
||||
iter(obj)
|
||||
return True
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
def isProtobuf(obj):
|
||||
"""Really bad workaround to check if an object is an
|
||||
instance of a protobuf message"""
|
||||
return hasattr(obj, "MergeFrom")
|
||||
|
||||
def parseProtobufObj(obj):
|
||||
output = {}
|
||||
for (fd, value) in obj.ListFields():
|
||||
key = fd.name
|
||||
if isProtobuf(value):
|
||||
if not isIterable(value):
|
||||
output.update({key: parseProtobufObj(value)})
|
||||
else:
|
||||
output.update({
|
||||
key: [parseProtobufObj(i) for i in value]
|
||||
})
|
||||
else:
|
||||
output.update({key: value})
|
||||
return output
|
||||
|
||||
def readInt(byteArray, start):
|
||||
"""Read the byte array, starting from *start* position,
|
||||
@ -92,6 +68,30 @@ def hasSearchResponse(obj):
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def hasCluster(obj):
|
||||
try:
|
||||
return obj.HasField('cluster')
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def hasTosContent(tocResponse):
|
||||
try:
|
||||
return tocResponse.HasField('tosContent')
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def hasTosToken(tocResponse):
|
||||
try:
|
||||
return tocResponse.HasField('tosToken')
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def hasCookie(tocResponse):
|
||||
try:
|
||||
return tocResponse.HasField('cookie')
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def hasDoc(obj):
|
||||
# doc an be a single object or a
|
||||
# RepeatedComposite object
|
||||
|
67
test/test.py
67
test/test.py
@ -24,79 +24,60 @@ server.login(None, None, gsfId, authSubToken)
|
||||
|
||||
# SEARCH
|
||||
|
||||
apps = server.search('telegram', 34, None)
|
||||
|
||||
print('\nSearch suggestion for "fir"\n')
|
||||
print(server.searchSuggest('fir'))
|
||||
|
||||
print('nb_result: 34')
|
||||
print('number of results: %d' % len(apps))
|
||||
result = server.search('firefox', 34, None)
|
||||
for cluster in result:
|
||||
print("cluster: {}".format(cluster.get('docid')))
|
||||
for app in cluster.get('child'):
|
||||
print(" app: {}".format(app.get('docid')))
|
||||
|
||||
print('\nFound those apps:\n')
|
||||
for a in apps:
|
||||
print(a['docId'])
|
||||
|
||||
# HOME APPS
|
||||
|
||||
print('\nFetching apps from play store home\n')
|
||||
home = server.getHomeApps()
|
||||
result = server.getHomeApps()
|
||||
for cluster in result:
|
||||
print("cluster: {}".format(cluster.get('docid')))
|
||||
for app in cluster.get('child'):
|
||||
print(" app: {}".format(app.get('docid')))
|
||||
|
||||
for cat in home:
|
||||
print("cat {0} with {1} apps".format(cat.get('categoryId'),
|
||||
str(len(cat.get('apps')))))
|
||||
|
||||
# DOWNLOAD
|
||||
docid = apps[0]['docId']
|
||||
print('\nTelegram docid is: %s\n' % docid)
|
||||
print('\nAttempting to download %s\n' % docid)
|
||||
docid = 'org.mozilla.focus'
|
||||
server.log(docid)
|
||||
print('\nAttempting to download {}\n'.format(docid))
|
||||
fl = server.download(docid)
|
||||
with open(docid + '.apk', 'wb') as apk_file:
|
||||
for chunk in fl.get('file').get('data'):
|
||||
apk_file.write(chunk)
|
||||
print('\nDownload successful\n')
|
||||
|
||||
# DOWNLOAD APP NOT PURCHASED
|
||||
# Attempting to download Nova Launcher Prime
|
||||
# it should throw an error 'App Not Purchased'
|
||||
|
||||
print('\nAttempting to download "com.teslacoilsw.launcher.prime"\n')
|
||||
errorThrown = False
|
||||
try:
|
||||
app = server.search('nova launcher prime', 3, None)
|
||||
app = filter(lambda x: x['docId'] == 'com.teslacoilsw.launcher.prime', app)
|
||||
app = list(app)[0]
|
||||
fl = server.delivery(app['docId'], app['versionCode'])
|
||||
except RequestError as e:
|
||||
errorThrown = True
|
||||
print(e)
|
||||
|
||||
if not errorThrown:
|
||||
print('Download of previous app should have failed')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# BULK DETAILS
|
||||
testApps = ['org.mozilla.focus', 'com.non.existing.app']
|
||||
bulk = server.bulkDetails(testApps)
|
||||
|
||||
print('\nTesting behaviour for non-existing apps\n')
|
||||
if bulk[1] is not None:
|
||||
print('bulkDetails should return None for non-existing apps')
|
||||
print('bulkDetails should return empty dict for non-existing apps')
|
||||
sys.exit(1)
|
||||
|
||||
print('\nResult from bulkDetails for %s\n' % testApps[0])
|
||||
print(bulk[0])
|
||||
print('\nResult from bulkDetails for {}\n'.format(testApps[0]))
|
||||
print(bulk[0]['docid'])
|
||||
|
||||
# DETAILS
|
||||
print('\nGetting details for %s\n' % testApps[0])
|
||||
details = server.details(testApps[0])
|
||||
print(details)
|
||||
print(details['title'])
|
||||
|
||||
# REVIEWS
|
||||
print('\nGetting reviews for %s\n' % testApps[0])
|
||||
revs = server.reviews(testApps[0])
|
||||
for r in revs:
|
||||
print(r['comment'])
|
||||
print("UserId: {0} Vote: {1}".format(
|
||||
r['userProfile']['personIdString'],
|
||||
str(r['starRating'])))
|
||||
|
||||
# BROWSE
|
||||
|
||||
@ -105,8 +86,10 @@ browse = server.browse()
|
||||
for b in browse:
|
||||
print(b['name'])
|
||||
|
||||
print('\nBrowsing the %s category\n' % browse[0]['catId'])
|
||||
browseCat = server.browse(browse[0]['catId'])
|
||||
sampleCat = browse[0]['unknownCategoryContainer']['categoryIdContainer']['categoryId']
|
||||
print('\nBrowsing the {} category\n'.format(sampleCat))
|
||||
browseCat = server.browse(sampleCat)
|
||||
|
||||
for b in browseCat:
|
||||
print('%s subcategory with %d apps' % (b['title'], len(b['apps'])))
|
||||
|
||||
@ -121,4 +104,4 @@ for c in catList:
|
||||
print('\nList %s apps for %s category\n' % (catList[0], cat))
|
||||
appList = server.list(cat, catList[0])
|
||||
for app in appList:
|
||||
print(app['docId'])
|
||||
print(app['docid'])
|
||||
|
Loading…
Reference in New Issue
Block a user