mirror of
https://github.com/FliegendeWurst/googleplay-api.git
synced 2024-11-22 04:44:59 +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";
|
syntax = "proto2";
|
||||||
|
|
||||||
|
// Both sha1 and sha256 are encoded with base64 with URL and Filename Safe Alphabet with padding removed
|
||||||
message AndroidAppDeliveryData {
|
message AndroidAppDeliveryData {
|
||||||
optional int64 downloadSize = 1;
|
optional int64 downloadSize = 1;
|
||||||
optional string signature = 2;
|
optional string sha1 = 2;
|
||||||
optional string downloadUrl = 3;
|
optional string downloadUrl = 3;
|
||||||
repeated AppFileMetadata additionalFile = 4;
|
repeated AppFileMetadata additionalFile = 4;
|
||||||
repeated HttpCookie downloadAuthCookie = 5;
|
repeated HttpCookie downloadAuthCookie = 5;
|
||||||
@ -13,10 +14,23 @@ message AndroidAppDeliveryData {
|
|||||||
optional bool immediateStartNeeded = 10;
|
optional bool immediateStartNeeded = 10;
|
||||||
optional AndroidAppPatchData patchData = 11;
|
optional AndroidAppPatchData patchData = 11;
|
||||||
optional EncryptionParams encryptionParams = 12;
|
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 {
|
message AndroidAppPatchData {
|
||||||
optional int32 baseVersionCode = 1;
|
optional int32 baseVersionCode = 1;
|
||||||
optional string baseSignature = 2;
|
optional string baseSha1 = 2;
|
||||||
optional string downloadUrl = 3;
|
optional string downloadUrl = 3;
|
||||||
optional int32 patchFormat = 4;
|
optional int32 patchFormat = 4;
|
||||||
optional int64 maxPatchSize = 5;
|
optional int64 maxPatchSize = 5;
|
||||||
@ -26,6 +40,9 @@ message AppFileMetadata {
|
|||||||
optional int32 versionCode = 2;
|
optional int32 versionCode = 2;
|
||||||
optional int64 size = 3;
|
optional int64 size = 3;
|
||||||
optional string downloadUrl = 4;
|
optional string downloadUrl = 4;
|
||||||
|
optional int64 sizeGzipped = 6;
|
||||||
|
optional string downloadUrlGzipped = 7;
|
||||||
|
optional string sha1 = 8;
|
||||||
}
|
}
|
||||||
message EncryptionParams {
|
message EncryptionParams {
|
||||||
optional int32 version = 1;
|
optional int32 version = 1;
|
||||||
@ -232,7 +249,10 @@ message Offer {
|
|||||||
optional SubscriptionTerms subscriptionTerms = 12;
|
optional SubscriptionTerms subscriptionTerms = 12;
|
||||||
optional string formattedName = 13;
|
optional string formattedName = 13;
|
||||||
optional string formattedDescription = 14;
|
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 {
|
message OwnershipInfo {
|
||||||
optional int64 initiationTimestampMsec = 1;
|
optional int64 initiationTimestampMsec = 1;
|
||||||
@ -353,11 +373,34 @@ message DetailsResponse {
|
|||||||
optional DocV2 docV2 = 4;
|
optional DocV2 docV2 = 4;
|
||||||
optional string footerHtml = 5;
|
optional string footerHtml = 5;
|
||||||
repeated Badge badge = 7;
|
repeated Badge badge = 7;
|
||||||
|
optional Features features = 12;
|
||||||
|
optional string detailsStreamUrl = 13;
|
||||||
|
optional string userReviewUrl = 14;
|
||||||
|
optional string postAcquireDetailsStreamUrl = 17;
|
||||||
}
|
}
|
||||||
message Badge {
|
message Badge {
|
||||||
optional string label = 1;
|
optional string label = 1;
|
||||||
|
optional Image image = 2;
|
||||||
|
optional BadgeContainer1 badgeContainer1 = 4;
|
||||||
optional string message = 11;
|
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 {
|
message DeviceConfigurationProto {
|
||||||
optional int32 touchScreen = 1;
|
optional int32 touchScreen = 1;
|
||||||
optional int32 keyboard = 2;
|
optional int32 keyboard = 2;
|
||||||
@ -425,6 +468,7 @@ message Image {
|
|||||||
optional string url = 12;
|
optional string url = 12;
|
||||||
}
|
}
|
||||||
optional string color = 15;
|
optional string color = 15;
|
||||||
|
optional int32 screenshotSetNumber = 21;
|
||||||
}
|
}
|
||||||
message TranslatedText {
|
message TranslatedText {
|
||||||
optional string text = 1;
|
optional string text = 1;
|
||||||
@ -467,10 +511,13 @@ message AppDetails {
|
|||||||
repeated FileMetadata file = 17;
|
repeated FileMetadata file = 17;
|
||||||
optional string appType = 18;
|
optional string appType = 18;
|
||||||
optional bool unstable = 21;
|
optional bool unstable = 21;
|
||||||
|
optional bool hasInstantLink = 24;
|
||||||
optional string containsAds = 30;
|
optional string containsAds = 30;
|
||||||
optional Dependencies dependencies = 34;
|
optional Dependencies dependencies = 34;
|
||||||
optional TestingProgramInfo testingProgramInfo = 35;
|
optional TestingProgramInfo testingProgramInfo = 35;
|
||||||
optional EarlyAccessInfo earlyAccessInfo = 36;
|
optional EarlyAccessInfo earlyAccessInfo = 36;
|
||||||
|
optional string instantLink = 43;
|
||||||
|
optional string developerAddress = 45;
|
||||||
}
|
}
|
||||||
message Dependencies {
|
message Dependencies {
|
||||||
optional int32 unknown1 = 1;
|
optional int32 unknown1 = 1;
|
||||||
@ -610,7 +657,7 @@ message Bucket {
|
|||||||
}
|
}
|
||||||
message ListResponse {
|
message ListResponse {
|
||||||
repeated Bucket bucket = 1;
|
repeated Bucket bucket = 1;
|
||||||
repeated SearchCluster cluster = 2;
|
repeated DocV2 doc = 2;
|
||||||
}
|
}
|
||||||
message DocV1 {
|
message DocV1 {
|
||||||
optional Document finskyDoc = 1;
|
optional Document finskyDoc = 1;
|
||||||
@ -655,7 +702,11 @@ message DocV2 {
|
|||||||
optional string purchaseDetailsUrl = 20;
|
optional string purchaseDetailsUrl = 20;
|
||||||
optional bool detailsReusable = 21;
|
optional bool detailsReusable = 21;
|
||||||
optional string subtitle = 22;
|
optional string subtitle = 22;
|
||||||
|
optional UnknownCategoryContainer unknownCategoryContainer = 24;
|
||||||
optional Unknown25 unknown25 = 25;
|
optional Unknown25 unknown25 = 25;
|
||||||
|
optional string descriptionShort = 27;
|
||||||
|
optional string reviewSnippetsUrl = 31;
|
||||||
|
optional string reviewQuestionsUrl = 34;
|
||||||
}
|
}
|
||||||
message Unknown25 {
|
message Unknown25 {
|
||||||
repeated Unknown25Item item = 2;
|
repeated Unknown25Item item = 2;
|
||||||
@ -679,6 +730,7 @@ message RelatedLinksUnknown1 {
|
|||||||
optional RelatedLinksUnknown2 unknown2 = 2;
|
optional RelatedLinksUnknown2 unknown2 = 2;
|
||||||
}
|
}
|
||||||
message RelatedLinksUnknown2 {
|
message RelatedLinksUnknown2 {
|
||||||
|
optional string homeUrl = 2;
|
||||||
optional string nextPageUrl = 3;
|
optional string nextPageUrl = 3;
|
||||||
}
|
}
|
||||||
message Rated {
|
message Rated {
|
||||||
@ -830,19 +882,74 @@ message AggregateRating {
|
|||||||
optional uint64 commentCount = 11;
|
optional uint64 commentCount = 11;
|
||||||
optional double bayesianMeanRating = 12;
|
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 {
|
message Payload {
|
||||||
optional ListResponse listResponse = 1;
|
optional ListResponse listResponse = 1;
|
||||||
optional DetailsResponse detailsResponse = 2;
|
optional DetailsResponse detailsResponse = 2;
|
||||||
optional ReviewResponse reviewResponse = 3;
|
optional ReviewResponse reviewResponse = 3;
|
||||||
optional BuyResponse buyResponse = 4;
|
optional BuyResponse buyResponse = 4;
|
||||||
optional SearchResponse searchResponse = 5;
|
optional SearchResponse searchResponse = 5;
|
||||||
|
optional TocResponse tocResponse = 6;
|
||||||
optional BrowseResponse browseResponse = 7;
|
optional BrowseResponse browseResponse = 7;
|
||||||
optional PurchaseStatusResponse purchaseStatusResponse = 8;
|
optional PurchaseStatusResponse purchaseStatusResponse = 8;
|
||||||
optional string logResponse = 10;
|
optional string logResponse = 10;
|
||||||
|
optional string flagContentResponse = 13;
|
||||||
optional BulkDetailsResponse bulkDetailsResponse = 19;
|
optional BulkDetailsResponse bulkDetailsResponse = 19;
|
||||||
optional DeliveryResponse deliveryResponse = 21;
|
optional DeliveryResponse deliveryResponse = 21;
|
||||||
optional UploadDeviceConfigResponse uploadDeviceConfigResponse = 25;
|
optional AcceptTosResponse acceptTosResponse = 22;
|
||||||
optional AndroidCheckinResponse androidCheckinResponse = 26;
|
optional AndroidCheckinResponse androidCheckinResponse = 26;
|
||||||
|
optional UploadDeviceConfigResponse uploadDeviceConfigResponse = 28;
|
||||||
optional SearchSuggestResponse searchSuggestResponse = 40;
|
optional SearchSuggestResponse searchSuggestResponse = 40;
|
||||||
optional TestingProgramResponse testingProgramResponse = 80;
|
optional TestingProgramResponse testingProgramResponse = 80;
|
||||||
}
|
}
|
||||||
@ -853,11 +960,40 @@ message PreFetch {
|
|||||||
optional int64 ttl = 4;
|
optional int64 ttl = 4;
|
||||||
optional int64 softTtl = 5;
|
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 {
|
message ResponseWrapper {
|
||||||
optional Payload payload = 1;
|
optional Payload payload = 1;
|
||||||
optional ServerCommands commands = 2;
|
optional ServerCommands commands = 2;
|
||||||
repeated PreFetch preFetch = 3;
|
repeated PreFetch preFetch = 3;
|
||||||
repeated Notification notification = 4;
|
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 {
|
message ServerCommands {
|
||||||
optional bool clearCache = 1;
|
optional bool clearCache = 1;
|
||||||
@ -881,30 +1017,23 @@ message Review {
|
|||||||
optional string deviceName = 19;
|
optional string deviceName = 19;
|
||||||
optional string replyText = 29;
|
optional string replyText = 29;
|
||||||
optional int64 replyTimestampMsec = 30;
|
optional int64 replyTimestampMsec = 30;
|
||||||
optional Author author = 31;
|
optional ReviewAuthor author = 31;
|
||||||
optional Author2 author2 = 33;
|
optional UserProfile userProfile = 33;
|
||||||
}
|
}
|
||||||
message Author {
|
message ReviewAuthor {
|
||||||
optional string name = 2;
|
optional string name = 2;
|
||||||
optional Avatar urls = 5;
|
optional Image avatar = 5;
|
||||||
}
|
}
|
||||||
message Author2 {
|
message UserProfile {
|
||||||
optional string personIdString = 1;
|
optional string personIdString = 1;
|
||||||
optional string personId = 2;
|
optional string personId = 2;
|
||||||
optional int32 unknown1 = 3;
|
optional int32 unknown1 = 3;
|
||||||
optional int32 unknown2 = 4;
|
optional int32 unknown2 = 4;
|
||||||
optional string name = 5;
|
optional string name = 5;
|
||||||
optional Avatar urls = 10;
|
repeated Image image = 10;
|
||||||
optional string googlePlusUrl = 19;
|
optional string googlePlusUrl = 19;
|
||||||
optional string googlePlusTagline = 22;
|
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 {
|
message ReviewResponse {
|
||||||
optional GetReviewsResponse getResponse = 1;
|
optional GetReviewsResponse getResponse = 1;
|
||||||
optional string nextPageUrl = 2;
|
optional string nextPageUrl = 2;
|
||||||
@ -927,18 +1056,6 @@ message SearchResponse {
|
|||||||
repeated RelatedSearch relatedSearch = 6;
|
repeated RelatedSearch relatedSearch = 6;
|
||||||
optional string nextPageUrl = 10;
|
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 {
|
message SearchSuggestResponse {
|
||||||
repeated SearchSuggestEntry entry = 1;
|
repeated SearchSuggestEntry entry = 1;
|
||||||
}
|
}
|
||||||
@ -1018,6 +1135,7 @@ message AndroidCheckinResponse {
|
|||||||
optional fixed64 securityToken = 8;
|
optional fixed64 securityToken = 8;
|
||||||
optional bool settingsDiff = 9;
|
optional bool settingsDiff = 9;
|
||||||
repeated string deleteSetting = 10;
|
repeated string deleteSetting = 10;
|
||||||
|
optional string deviceCheckinConsistencyToken = 12;
|
||||||
}
|
}
|
||||||
message GservicesSetting {
|
message GservicesSetting {
|
||||||
optional bytes name = 1;
|
optional bytes name = 1;
|
||||||
@ -1139,11 +1257,21 @@ message StatCounters {
|
|||||||
message UsageStatsExtensionProto {
|
message UsageStatsExtensionProto {
|
||||||
optional AndroidDataUsageProto dataUsage = 1;
|
optional AndroidDataUsageProto dataUsage = 1;
|
||||||
}
|
}
|
||||||
message SearchCluster {
|
message ModifyLibraryRequest {
|
||||||
optional string id = 1;
|
optional string libraryId = 1;
|
||||||
optional string type = 2;
|
repeated string addPackageName = 2;
|
||||||
optional int64 int1 = 3;
|
repeated string removePackageName = 3;
|
||||||
optional int64 int2 = 4;
|
}
|
||||||
optional string category = 5;
|
message UrlRequestWrapper {
|
||||||
repeated DocV2 doc = 11;
|
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"
|
BULK_URL = FDFE + "bulkDetails"
|
||||||
LOG_URL = FDFE + "log"
|
LOG_URL = FDFE + "log"
|
||||||
TOC_URL = FDFE + "toc"
|
TOC_URL = FDFE + "toc"
|
||||||
|
ACCEPT_TOS_URL = FDFE + "acceptTos"
|
||||||
LIST_URL = FDFE + "list"
|
LIST_URL = FDFE + "list"
|
||||||
REVIEWS_URL = FDFE + "rev"
|
REVIEWS_URL = FDFE + "rev"
|
||||||
|
|
||||||
@ -72,6 +73,8 @@ class GooglePlayAPI(object):
|
|||||||
self.authSubToken = None
|
self.authSubToken = None
|
||||||
self.gsfId = None
|
self.gsfId = None
|
||||||
self.device_config_token = None
|
self.device_config_token = None
|
||||||
|
self.deviceCheckinConsistencyToken = None
|
||||||
|
self.dfeCookie = None
|
||||||
self.proxies_config = proxies_config
|
self.proxies_config = proxies_config
|
||||||
self.deviceBuilder = config.DeviceBuilder(device_codename)
|
self.deviceBuilder = config.DeviceBuilder(device_codename)
|
||||||
self.set_locale(locale)
|
self.set_locale(locale)
|
||||||
@ -141,6 +144,10 @@ class GooglePlayAPI(object):
|
|||||||
headers["Authorization"] = "GoogleLogin auth=%s" % self.authSubToken
|
headers["Authorization"] = "GoogleLogin auth=%s" % self.authSubToken
|
||||||
if self.device_config_token is not None:
|
if self.device_config_token is not None:
|
||||||
headers["X-DFE-Device-Config-Token"] = self.device_config_token
|
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
|
return headers
|
||||||
|
|
||||||
def checkin(self, email, ac2dmToken):
|
def checkin(self, email, ac2dmToken):
|
||||||
@ -155,6 +162,7 @@ class GooglePlayAPI(object):
|
|||||||
proxies=self.proxies_config)
|
proxies=self.proxies_config)
|
||||||
response = googleplay_pb2.AndroidCheckinResponse()
|
response = googleplay_pb2.AndroidCheckinResponse()
|
||||||
response.ParseFromString(res.content)
|
response.ParseFromString(res.content)
|
||||||
|
self.deviceCheckinConsistencyToken = response.deviceCheckinConsistencyToken
|
||||||
|
|
||||||
# checkin again to upload gfsid
|
# checkin again to upload gfsid
|
||||||
request.id = response.androidId
|
request.id = response.androidId
|
||||||
@ -226,7 +234,7 @@ class GooglePlayAPI(object):
|
|||||||
if "NeedsBrowser" in params["error"]:
|
if "NeedsBrowser" in params["error"]:
|
||||||
raise SecurityCheckError("Security check is needed, try to visit "
|
raise SecurityCheckError("Security check is needed, try to visit "
|
||||||
"https://accounts.google.com/b/0/DisplayUnlockCaptcha "
|
"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"])
|
raise LoginError("server says: " + params["error"])
|
||||||
else:
|
else:
|
||||||
raise LoginError("Auth token not found.")
|
raise LoginError("Auth token not found.")
|
||||||
@ -239,7 +247,7 @@ class GooglePlayAPI(object):
|
|||||||
self.gsfId = gsfId
|
self.gsfId = gsfId
|
||||||
self.setAuthSubToken(authSubToken)
|
self.setAuthSubToken(authSubToken)
|
||||||
# check if token is valid with a simple search
|
# check if token is valid with a simple search
|
||||||
self.search('firefox', 1, None)
|
self.search('drv', 1, None)
|
||||||
else:
|
else:
|
||||||
raise LoginError('Either (email,pass) or (gsfId, authSubToken) is needed')
|
raise LoginError('Either (email,pass) or (gsfId, authSubToken) is needed')
|
||||||
|
|
||||||
@ -335,10 +343,10 @@ class GooglePlayAPI(object):
|
|||||||
"ssis": "120",
|
"ssis": "120",
|
||||||
"sst": "2"}
|
"sst": "2"}
|
||||||
data = self.executeRequestApi2(SEARCH_SUGGEST_URL, params=params)
|
data = self.executeRequestApi2(SEARCH_SUGGEST_URL, params=params)
|
||||||
response = data.payload.searchSuggestResponse
|
output = []
|
||||||
return [{"type": e.type,
|
for entry in data.payload.searchSuggestResponse.entry:
|
||||||
"suggestedQuery": e.suggestedQuery,
|
output.append(utils.parseProtobufObj(entry))
|
||||||
"title": e.title} for e in response.entry]
|
return output
|
||||||
|
|
||||||
def search(self, query, nb_result, offset=None):
|
def search(self, query, nb_result, offset=None):
|
||||||
""" Search the play store for an app.
|
""" Search the play store for an app.
|
||||||
@ -347,49 +355,26 @@ class GooglePlayAPI(object):
|
|||||||
|
|
||||||
offset is used to take result starting from an index.
|
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:
|
if self.authSubToken is None:
|
||||||
raise Exception("You need to login before executing any request")
|
raise Exception("You need to login before executing any request")
|
||||||
|
|
||||||
remaining = nb_result
|
remaining = nb_result
|
||||||
output = []
|
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):
|
if (offset is not None):
|
||||||
nextPath += "&o={}".format(offset)
|
nextPath += "&o={}".format(offset)
|
||||||
while remaining > 0 and nextPath is not None:
|
# TODO: not sure if this toc call should be here
|
||||||
currentPath = nextPath
|
self.toc()
|
||||||
data = self.executeRequestApi2(currentPath)
|
data = self.executeRequestApi2(path)
|
||||||
if utils.hasPrefetch(data):
|
if utils.hasPrefetch(data):
|
||||||
response = data.preFetch[0].response
|
response = data.preFetch[0].response
|
||||||
else:
|
else:
|
||||||
response = data
|
response = data
|
||||||
if utils.hasSearchResponse(response.payload):
|
output = []
|
||||||
# we still need to fetch the first page, so go to
|
for cluster in response.payload.listResponse.doc[0].child:
|
||||||
# next loop iteration without decrementing counter
|
output.append(utils.parseProtobufObj(cluster))
|
||||||
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]
|
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def details(self, packageName):
|
def details(self, packageName):
|
||||||
@ -398,7 +383,7 @@ class GooglePlayAPI(object):
|
|||||||
packageName is the app unique ID (usually starting with 'com.')."""
|
packageName is the app unique ID (usually starting with 'com.')."""
|
||||||
path = DETAILS_URL + "?doc={}".format(requests.utils.quote(packageName))
|
path = DETAILS_URL + "?doc={}".format(requests.utils.quote(packageName))
|
||||||
data = self.executeRequestApi2(path)
|
data = self.executeRequestApi2(path)
|
||||||
return utils.fromDocToDictionary(data.payload.detailsResponse.docV2)
|
return utils.parseProtobufObj(data.payload.detailsResponse.docV2)
|
||||||
|
|
||||||
def bulkDetails(self, packageNames):
|
def bulkDetails(self, packageNames):
|
||||||
"""Get several apps details from a list of package names.
|
"""Get several apps details from a list of package names.
|
||||||
@ -424,18 +409,19 @@ class GooglePlayAPI(object):
|
|||||||
params=params)
|
params=params)
|
||||||
response = message.payload.bulkDetailsResponse
|
response = message.payload.bulkDetailsResponse
|
||||||
return [None if not utils.hasDoc(entry) else
|
return [None if not utils.hasDoc(entry) else
|
||||||
utils.fromDocToDictionary(entry.doc)
|
utils.parseProtobufObj(entry.doc)
|
||||||
for entry in response.entry]
|
for entry in response.entry]
|
||||||
|
|
||||||
def getHomeApps(self):
|
def getHomeApps(self):
|
||||||
path = HOME_URL + "?c=3&nocache_isui=true"
|
path = HOME_URL + "?c=3&nocache_isui=true"
|
||||||
data = self.executeRequestApi2(path)
|
data = self.executeRequestApi2(path)
|
||||||
|
if utils.hasPrefetch(data):
|
||||||
|
response = data.preFetch[0].response
|
||||||
|
else:
|
||||||
|
response = data
|
||||||
output = []
|
output = []
|
||||||
cluster = data.preFetch[0].response.payload.listResponse.cluster[0]
|
for cluster in response.payload.listResponse.doc[0].child:
|
||||||
for doc in cluster.doc:
|
output.append(utils.parseProtobufObj(cluster))
|
||||||
output.append({"categoryId": doc.docid,
|
|
||||||
"categoryStr": doc.title,
|
|
||||||
"apps": [utils.fromDocToDictionary(c) for c in doc.child]})
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def browse(self, cat=None, subCat=None):
|
def browse(self, cat=None, subCat=None):
|
||||||
@ -451,29 +437,26 @@ class GooglePlayAPI(object):
|
|||||||
|
|
||||||
if cat is None and subCat is None:
|
if cat is None and subCat is None:
|
||||||
# result contains all categories available
|
# result contains all categories available
|
||||||
return [{'name': c.name,
|
return [utils.parseProtobufObj(c) for c in data.payload.browseResponse.category]
|
||||||
'dataUrl': c.dataUrl,
|
|
||||||
'catId': c.unknownCategoryContainer.categoryIdContainer.categoryId}
|
|
||||||
for c in data.payload.browseResponse.category]
|
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
clusters = []
|
clusters = []
|
||||||
|
|
||||||
if utils.hasPrefetch(data):
|
if utils.hasPrefetch(data):
|
||||||
for pf in data.preFetch:
|
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
|
# result contains apps of a specific category
|
||||||
# organized by sections
|
# organized by sections
|
||||||
for cluster in clusters:
|
for cluster in clusters:
|
||||||
for doc in cluster.doc:
|
apps = [a for a in cluster.child]
|
||||||
apps = [a for a in doc.child]
|
apps = list(map(utils.parseProtobufObj,
|
||||||
apps = list(map(utils.fromDocToDictionary,
|
apps))
|
||||||
apps))
|
section = {'title': cluster.title,
|
||||||
section = {'title': doc.title,
|
'docid': cluster.docid,
|
||||||
'docid': doc.docid,
|
'apps': apps}
|
||||||
'apps': apps}
|
output.append(section)
|
||||||
output.append(section)
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def list(self, cat, ctr=None, nb_results=None, offset=None):
|
def list(self, cat, ctr=None, nb_results=None, offset=None):
|
||||||
@ -495,19 +478,16 @@ class GooglePlayAPI(object):
|
|||||||
if ctr is None:
|
if ctr is None:
|
||||||
# list subcategories
|
# list subcategories
|
||||||
for pf in data.preFetch:
|
for pf in data.preFetch:
|
||||||
clusters.extend(pf.response.payload.listResponse.cluster)
|
for cluster in pf.response.payload.listResponse.doc:
|
||||||
for c in clusters:
|
clusters.extend(cluster.child)
|
||||||
docs.extend(c.doc)
|
return [c.docid for c in clusters]
|
||||||
return [d.docid for d in docs]
|
|
||||||
else:
|
else:
|
||||||
childs = []
|
apps = []
|
||||||
clusters.extend(data.payload.listResponse.cluster)
|
for d in data.payload.listResponse.doc:
|
||||||
for c in clusters:
|
for c in d.child: # category
|
||||||
docs.extend(c.doc)
|
for a in c.child: # app
|
||||||
for d in docs:
|
apps.append(utils.parseProtobufObj(a))
|
||||||
childs.extend(d.child)
|
return apps
|
||||||
return [utils.fromDocToDictionary(c)
|
|
||||||
for c in childs]
|
|
||||||
|
|
||||||
def reviews(self, packageName, filterByDevice=False, sort=2,
|
def reviews(self, packageName, filterByDevice=False, sort=2,
|
||||||
nb_results=None, offset=None):
|
nb_results=None, offset=None):
|
||||||
@ -524,6 +504,7 @@ class GooglePlayAPI(object):
|
|||||||
dict object containing all the protobuf data returned from
|
dict object containing all the protobuf data returned from
|
||||||
the api
|
the api
|
||||||
"""
|
"""
|
||||||
|
# TODO: select the number of reviews to return
|
||||||
path = REVIEWS_URL + "?doc={}&sort={}".format(requests.utils.quote(packageName), sort)
|
path = REVIEWS_URL + "?doc={}&sort={}".format(requests.utils.quote(packageName), sort)
|
||||||
if nb_results is not None:
|
if nb_results is not None:
|
||||||
path += "&n={}".format(nb_results)
|
path += "&n={}".format(nb_results)
|
||||||
@ -533,19 +514,8 @@ class GooglePlayAPI(object):
|
|||||||
path += "&dfil=1"
|
path += "&dfil=1"
|
||||||
data = self.executeRequestApi2(path)
|
data = self.executeRequestApi2(path)
|
||||||
output = []
|
output = []
|
||||||
for rev in data.payload.reviewResponse.getResponse.review:
|
for review in data.payload.reviewResponse.getResponse.review:
|
||||||
author = {'personIdString': rev.author2.personIdString,
|
output.append(utils.parseProtobufObj(review))
|
||||||
'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)
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def _deliver_data(self, url, cookies):
|
def _deliver_data(self, url, cookies):
|
||||||
@ -650,7 +620,8 @@ class GooglePlayAPI(object):
|
|||||||
|
|
||||||
if versionCode is None:
|
if versionCode is None:
|
||||||
# pick up latest version
|
# 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()
|
headers = self.getHeaders()
|
||||||
params = {'ot': str(offerType),
|
params = {'ot': str(offerType),
|
||||||
@ -687,6 +658,35 @@ class GooglePlayAPI(object):
|
|||||||
if response.commands.displayErrorMessage != "":
|
if response.commands.displayErrorMessage != "":
|
||||||
raise RequestError(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
|
@staticmethod
|
||||||
def getDevicesCodenames():
|
def getDevicesCodenames():
|
||||||
return config.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 struct
|
||||||
import sys
|
import sys
|
||||||
|
from google.protobuf.message import Message
|
||||||
from . import googleplay_pb2
|
from . import googleplay_pb2
|
||||||
|
|
||||||
VERSION = sys.version_info[0]
|
VERSION = sys.version_info[0]
|
||||||
|
|
||||||
def fromDocToDictionary(app):
|
def isIterable(obj):
|
||||||
return {"docId": app.docid,
|
try:
|
||||||
"title": app.title,
|
iter(obj)
|
||||||
"author": app.creator,
|
return True
|
||||||
"description": app.descriptionHtml,
|
except TypeError:
|
||||||
"recentChanges": app.details.appDetails.recentChangesHtml,
|
return False
|
||||||
"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 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):
|
def readInt(byteArray, start):
|
||||||
"""Read the byte array, starting from *start* position,
|
"""Read the byte array, starting from *start* position,
|
||||||
@ -92,6 +68,30 @@ def hasSearchResponse(obj):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
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):
|
def hasDoc(obj):
|
||||||
# doc an be a single object or a
|
# doc an be a single object or a
|
||||||
# RepeatedComposite object
|
# RepeatedComposite object
|
||||||
|
67
test/test.py
67
test/test.py
@ -24,79 +24,60 @@ server.login(None, None, gsfId, authSubToken)
|
|||||||
|
|
||||||
# SEARCH
|
# SEARCH
|
||||||
|
|
||||||
apps = server.search('telegram', 34, None)
|
|
||||||
|
|
||||||
print('\nSearch suggestion for "fir"\n')
|
print('\nSearch suggestion for "fir"\n')
|
||||||
print(server.searchSuggest('fir'))
|
print(server.searchSuggest('fir'))
|
||||||
|
|
||||||
print('nb_result: 34')
|
result = server.search('firefox', 34, None)
|
||||||
print('number of results: %d' % len(apps))
|
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
|
# HOME APPS
|
||||||
|
|
||||||
print('\nFetching apps from play store home\n')
|
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
|
# DOWNLOAD
|
||||||
docid = apps[0]['docId']
|
docid = 'org.mozilla.focus'
|
||||||
print('\nTelegram docid is: %s\n' % docid)
|
server.log(docid)
|
||||||
print('\nAttempting to download %s\n' % docid)
|
print('\nAttempting to download {}\n'.format(docid))
|
||||||
fl = server.download(docid)
|
fl = server.download(docid)
|
||||||
with open(docid + '.apk', 'wb') as apk_file:
|
with open(docid + '.apk', 'wb') as apk_file:
|
||||||
for chunk in fl.get('file').get('data'):
|
for chunk in fl.get('file').get('data'):
|
||||||
apk_file.write(chunk)
|
apk_file.write(chunk)
|
||||||
print('\nDownload successful\n')
|
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
|
# BULK DETAILS
|
||||||
testApps = ['org.mozilla.focus', 'com.non.existing.app']
|
testApps = ['org.mozilla.focus', 'com.non.existing.app']
|
||||||
bulk = server.bulkDetails(testApps)
|
bulk = server.bulkDetails(testApps)
|
||||||
|
|
||||||
print('\nTesting behaviour for non-existing apps\n')
|
print('\nTesting behaviour for non-existing apps\n')
|
||||||
if bulk[1] is not None:
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
print('\nResult from bulkDetails for %s\n' % testApps[0])
|
print('\nResult from bulkDetails for {}\n'.format(testApps[0]))
|
||||||
print(bulk[0])
|
print(bulk[0]['docid'])
|
||||||
|
|
||||||
# DETAILS
|
# DETAILS
|
||||||
print('\nGetting details for %s\n' % testApps[0])
|
print('\nGetting details for %s\n' % testApps[0])
|
||||||
details = server.details(testApps[0])
|
details = server.details(testApps[0])
|
||||||
print(details)
|
print(details['title'])
|
||||||
|
|
||||||
# REVIEWS
|
# REVIEWS
|
||||||
print('\nGetting reviews for %s\n' % testApps[0])
|
print('\nGetting reviews for %s\n' % testApps[0])
|
||||||
revs = server.reviews(testApps[0])
|
revs = server.reviews(testApps[0])
|
||||||
for r in revs:
|
for r in revs:
|
||||||
print(r['comment'])
|
print("UserId: {0} Vote: {1}".format(
|
||||||
|
r['userProfile']['personIdString'],
|
||||||
|
str(r['starRating'])))
|
||||||
|
|
||||||
# BROWSE
|
# BROWSE
|
||||||
|
|
||||||
@ -105,8 +86,10 @@ browse = server.browse()
|
|||||||
for b in browse:
|
for b in browse:
|
||||||
print(b['name'])
|
print(b['name'])
|
||||||
|
|
||||||
print('\nBrowsing the %s category\n' % browse[0]['catId'])
|
sampleCat = browse[0]['unknownCategoryContainer']['categoryIdContainer']['categoryId']
|
||||||
browseCat = server.browse(browse[0]['catId'])
|
print('\nBrowsing the {} category\n'.format(sampleCat))
|
||||||
|
browseCat = server.browse(sampleCat)
|
||||||
|
|
||||||
for b in browseCat:
|
for b in browseCat:
|
||||||
print('%s subcategory with %d apps' % (b['title'], len(b['apps'])))
|
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))
|
print('\nList %s apps for %s category\n' % (catList[0], cat))
|
||||||
appList = server.list(cat, catList[0])
|
appList = server.list(cat, catList[0])
|
||||||
for app in appList:
|
for app in appList:
|
||||||
print(app['docId'])
|
print(app['docid'])
|
||||||
|
Loading…
Reference in New Issue
Block a user