├── .gitignore ├── README.md ├── TODO.txt ├── androguard └── androproto.py ├── apishell.py ├── categories.py ├── config.py ├── dbConfig.py ├── download.py ├── downloadFromList.py ├── googleplay.proto ├── googleplay.py ├── googleplay_pb2.py ├── helpers.py ├── list.py ├── permissions.py └── search.py /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore precompiled python files 2 | *.pyc 3 | 4 | # ignore editor leftovers 5 | *~ 6 | .*.sw* 7 | 8 | # ignore downloaded apps 9 | *.apk 10 | 11 | # ignore my configure file 12 | config.py 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Play Unofficial Python API 2 | The original code is at https://github.com/egirault/googleplay-api 3 | I revised it mainly for PrivacyGrade specific database setting. 4 | 5 | An unofficial Python API that let you search, browse and download Android apps from Google Play (formerly Android Market). 6 | 7 | This library is inspired by those projects, working with the old version of the API: 8 | 9 | * [Android Market Python API](https://github.com/liato/android-market-api-py) 10 | * [Android Market Java API](http://code.google.com/p/android-market-api/) 11 | 12 | ## Disclaimer 13 | **This is not an official API. I am not afiliated with Google in any way, and am not responsible of any damage that could be done with it. Use it at your own risk.** 14 | 15 | ## Dependencies 16 | * [Python 2.5+](http://www.python.org) 17 | * [Protocol Buffers](http://code.google.com/p/protobuf/) 18 | 19 | ## Requirements 20 | You must edit `config.py` before using the provided scripts (`search.py`, `download.py`, `apishell.py`, etc.). First, you need to provide your phone's `androidID`: 21 | 22 | ANDROID_ID = None # "xxxxxxxxxxxxxxxx" 23 | 24 | To get your `androidID`, use `*#*#8255#*#*` on your phone to start *Gtalk Monitor*. The hex string listed after `aid` is your `androidID`. 25 | 26 | In order to authenticate to Google Play, you also need to provide either your Google login and password, or a valid subAuthToken. 27 | 28 | ## Usage 29 | 30 | ### Searching 31 | 32 | $ python search.py 33 | Usage: search.py request [nb_results] [offset] 34 | Search for an app. 35 | If request contains a space, don't forget to surround it with "" 36 | 37 | $ python search.py earth 38 | Title;Package name;Creator;Super Dev;Price;Offer Type;Version Code;Size;Rating;Num Downloads 39 | Google Earth;com.google.earth;Google Inc.;1;Gratuit;1;53;8.6MB;4.46;10 000 000+ 40 | Terre HD Free Edition;ru.gonorovsky.kv.livewall.earthhd;Stanislav Gonorovsky;0;Gratuit;1;33;4.7MB;4.47;1 000 000+ 41 | Earth Live Wallpaper;com.seb.SLWP;unixseb;0;Gratuit;1;60;687.4KB;4.06;5 000 000+ 42 | Super Earth Wallpaper Free;com.mx.spacelwpfree;Mariux;0;Gratuit;1;2;1.8MB;4.41;100 000+ 43 | Earth And Legend;com.dvidearts.earthandlegend;DVide Arts Incorporated;0;5,99 €;1;6;6.8MB;4.82;50 000+ 44 | [...] 45 | 46 | Depending on the number of results you ask, you might get an error. My tests show that 100 search results are the maximum, but it may vary. 47 | 48 | By default, all scripts have CSV output. You can use Linux's `column` to prettify the output: 49 | 50 | $ alias pp="column -s ';' -t" 51 | $ python search.py earth | pp 52 | Title Package name Creator Super Dev Price Offer Type Version Code Size Rating Num Downloads 53 | Google Earth com.google.earth Google Inc. 1 Gratuit 1 53 8.6MB 4.46 10 000 000+ 54 | Terre HD Free Edition ru.gonorovsky.kv.livewall.earthhd Stanislav Gonorovsky 0 Gratuit 1 33 4.7MB 4.47 1 000 000+ 55 | Earth Live Wallpaper com.seb.SLWP unixseb 0 Gratuit 1 60 687.4KB 4.06 5 000 000+ 56 | Super Earth Wallpaper Free com.mx.spacelwpfree Mariux 0 Gratuit 1 2 1.8MB 4.41 100 000+ 57 | Earth And Legend com.dvidearts.earthandlegend DVide Arts Incorporated 0 5,99 € 1 6 6.8MB 4.82 50 000+ 58 | Earth 3D com.jmsys.earth3d Dokon Jang 0 Gratuit 1 12 3.4MB 4.05 500 000+ 59 | [...] 60 | 61 | ### Browse categories 62 | 63 | You can list all app categories this way: 64 | 65 | $ python categories.py | pp 66 | ID Name 67 | GAME Jeux 68 | NEWS_AND_MAGAZINES Actualités et magazines 69 | COMICS BD 70 | LIBRARIES_AND_DEMO Bibliothèques et démos 71 | COMMUNICATION Communication 72 | ENTERTAINMENT Divertissement 73 | EDUCATION Enseignement 74 | FINANCE Finance 75 | 76 | Sorry for non-French speakers! 77 | 78 | ### List subcategories and apps 79 | 80 | All categories have subcategories. You can list them with: 81 | 82 | $ python list.py 83 | Usage: list.py category [subcategory] [nb_results] [offset] 84 | List subcategories and apps within them. 85 | category: To obtain a list of supported catagories, use categories.py 86 | subcategory: You can get a list of all subcategories available, by supplying a valid category 87 | 88 | $ python list.py WEATHER | pp 89 | Subcategory ID Name 90 | apps_topselling_paid Top payant 91 | apps_topselling_free Top gratuit 92 | apps_topgrossing Les plus rentables 93 | apps_topselling_new_paid Top des nouveautés payantes 94 | apps_topselling_new_free Top des nouveautés gratuites 95 | 96 | And then list apps within them: 97 | 98 | $ python list.py WEATHER apps_topselling_free | pp 99 | Title Package name Creator Super Dev Price Offer Type Version Code Size Rating Num Downloads 100 | La chaine météo com.lachainemeteo.androidapp METEO CONSULT 0 Gratuit 1 8 4.6MB 4.38 1 000 000+ 101 | Météo-France fr.meteo Météo-France 0 Gratuit 1 11 2.4MB 3.63 1 000 000+ 102 | GO Weather EX com.gau.go.launcherex.gowidget.weatherwidget GO Launcher EX 0 Gratuit 1 25 6.5MB 4.40 10 000 000+ 103 | Thermomètre (Gratuit) com.xiaad.android.thermometertrial Mobiquité 0 Gratuit 1 60 3.6MB 3.78 1 000 000+ 104 | 105 | ### Viewing permissions 106 | 107 | You can use `permissions.py` to see what permissions are required by an app without downloading it: 108 | 109 | $ python search.py gmail 1 | pp 110 | Titre Package name Creator Super Dev Price Offer Type Version Code Size Rating Num Downloads 111 | Gmail com.google.android.gm Google Inc. 1 Gratuit 1 403 2.7MB 4.32 100 000 000+ 112 | 113 | $ python permissions.py com.google.android.gm 114 | android.permission.ACCESS_NETWORK_STATE 115 | android.permission.GET_ACCOUNTS 116 | android.permission.MANAGE_ACCOUNTS 117 | android.permission.INTERNET 118 | android.permission.READ_CONTACTS 119 | android.permission.WRITE_CONTACTS 120 | android.permission.READ_SYNC_SETTINGS 121 | android.permission.READ_SYNC_STATS 122 | android.permission.RECEIVE_BOOT_COMPLETED 123 | [...] 124 | 125 | You can specify multiple apps, using only one request. 126 | 127 | ### Downloading apps 128 | 129 | Downloading an app is really easy, just provide its package name. I only tested with free apps, but I guess it should work as well with non-free as soon as you have enough money on your Google account. 130 | 131 | $ python download.py com.google.android.gm 132 | Downloading 2.7MB... Done 133 | 134 | $ file com.google.android.gm.apk 135 | com.google.android.gm.apk: Zip archive data, at least v2.0 to extract 136 | 137 | ### Interactive shell 138 | An interactive shell can be started using the `apishell.py` script. It initializes the `api` object and logs you in. 139 | 140 | $ python apishell.py 141 | 142 | Google Play Unofficial API Interactive Shell 143 | Successfully logged in using your Google account. The variable 'api' holds the API object. 144 | Feel free to use help(api). 145 | 146 | >>> print api.__doc__ 147 | Google Play Unofficial API Class 148 | Usual APIs methods are login(), search(), details(), download(), browse() and list(). 149 | toStr() can be used to pretty print the result (protobuf object) of the previous methods. 150 | toDict() converts the result into a dict, for easier introspection. 151 | 152 | >>> res = api.search("angry birds") 153 | >>> for i in res.doc[0].child: 154 | ... print i.title.encode('utf8') 155 | ... 156 | Angry Birds 157 | Angry Birds Seasons 158 | Angry Birds Space 159 | Angry Birds Rio 160 | Angry Birds Space Premium 161 | Angry Birds - AngryBirdsBackup 162 | Angry Aviary LiteÔÿà Angry Birds 163 | [...] 164 | 165 | All results returned by methods such as `search()`, `details()`, ..., are Protobuf objects. You can use `toStr` and `toDict` method from `GooglePlayAPI` to pretty-print them and make introspection easier if you're not familiar with Protobuf. 166 | 167 | >>> s = api.browse() 168 | >>> s 169 | 170 | >>> d = api.toDict(s) 171 | >>> d.keys() 172 | ['promoUrl', 'category', 'contentsUrl'] 173 | >>> from pprint import pprint 174 | >>> pprint(d['category']) 175 | [{'dataUrl': u'browse?c=3&cat=GAME', 'name': u'Jeux'}, 176 | {'dataUrl': u'browse?c=3&cat=NEWS_AND_MAGAZINES', 177 | [...] 178 | 179 | ### Using the API as a module in another project 180 | 181 | You only need `googleplay.py` and `googleplay_pb2.py`. All other scripts are just front-ends. 182 | 183 | >>> from googleplay import GooglePlayAPI 184 | >>> help(GooglePlayAPI) 185 | 186 | What else? 187 | 188 | ### To be continued 189 | 190 | Feel free to extend the API, add command-line options to scripts, fork the project, and port it to any language. 191 | You can generate Protobuf stubs from `googleplay.proto` file with Google's `protoc`: 192 | 193 | $ protoc -h 194 | Usage: protoc [OPTION] PROTO_FILES 195 | Parse PROTO_FILES and generate output based on the options given: 196 | [...] 197 | --cpp_out=OUT_DIR Generate C++ header and source. 198 | --java_out=OUT_DIR Generate Java source file. 199 | --python_out=OUT_DIR Generate Python source file. 200 | 201 | ## License 202 | 203 | This project is released under the BSD license. 204 | 205 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | - Use optparse module for parsing arguments. 2 | - Add cli parameters to search.py and list.py to display only some columns. 3 | - Handle multiple output formats. 4 | - Handle app reviews. 5 | -------------------------------------------------------------------------------- /androguard/androproto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # This script analyzes an APK and tries to recover its .proto file, assuming 3 | # the APK is using Micro-Protobuf. It has only been tested on Google Play 4 | # Android client (sha1: 0f214c312f9800b01e2a5a7b9766dc880efda110). 5 | # 6 | # Use it at your own risk! 7 | 8 | import sys 9 | 10 | from pprint import pprint 11 | 12 | from androguard.core import * 13 | from androguard.core.androgen import * 14 | from androguard.core.androconf import * 15 | from androguard.core.bytecode import * 16 | from androguard.core.bytecodes.jvm import * 17 | from androguard.core.bytecodes.dvm import * 18 | from androguard.core.bytecodes.apk import * 19 | from androguard.core.analysis.analysis import * 20 | 21 | # Find mergeFrom() method in class with name cn 22 | def find_mergeFrom(dvm, cn): 23 | l = filter(lambda m: m.get_name() == "mergeFrom" and not m.get_descriptor().endswith("MessageMicro;"), dvm.get_methods_class(cn)) 24 | if (len(l) != 1): 25 | raise Exception("Unable to find mergeFrom() in class %s" % cn) 26 | return l[0] 27 | 28 | def index_basic_blocks(dvm, vma, cn): 29 | m = find_mergeFrom(dvm, cn) 30 | ma = vma.get_method(m) 31 | bbs = ma.basic_blocks.gets() 32 | 33 | # Find the basic block which ends with a sparse-switch (usually the first) 34 | l = filter(lambda bb: bb.get_instructions()[-1].get_name() == "sparse-switch", bbs) 35 | if (len(l) != 1): 36 | return {} # TODO 37 | # raise Exception("Unable to find a basic block ending with a sparse-switch in mergeFrom() method of class %s" % cn) 38 | # TODO handle packed-switch (cf 1ere classe dans proto_class_names) 39 | ss = l[0] 40 | 41 | # Get the offset of the sparse-switch, and the sparse-switch-payload 42 | # instruction. 43 | n = ss.get_nb_instructions() 44 | offset_ss = sum(i.get_length() for i in ss.get_instructions()[:n-1]) 45 | ssp = ss.get_special_ins(offset_ss) 46 | 47 | # Fill the list {key: bb} for this class 48 | d = {} 49 | for key, target in zip(ssp.get_keys(), ssp.get_targets()): 50 | d[key >> 3] = ma.basic_blocks.get_basic_block(offset_ss + target*2) 51 | 52 | return d 53 | 54 | def get_invoked_method_info(i): 55 | m = i.cm.get_method_ref(i.BBBB) 56 | return (m.get_class_name(), m.get_name(), m.get_descriptor()) 57 | 58 | def classname_to_messagename(cn): 59 | return cn.split('/')[-1].replace(';', '') 60 | 61 | def ulfirst(s): 62 | return s[0].lower() + s[1:] 63 | 64 | def analyse_bb(bb, k, cn): 65 | message_type = None 66 | l = [] 67 | 68 | # Index all invoke-virtual instructions. There should be 2 per basic block; 69 | # one for reading from the stream, the other for setting the appropriate 70 | # class member. 71 | for i in bb.get_instructions(): 72 | n = i.get_name() 73 | if n == "invoke-virtual": 74 | icn, imn, imd = get_invoked_method_info(i) 75 | l.append( imn ) # class name : icn.split("/")[-1] 76 | 77 | if n == "invoke-direct": 78 | icn, imn, _ = get_invoked_method_info(i) 79 | 80 | if (imn == ""): 81 | message_type = classname_to_messagename(icn) 82 | 83 | 84 | if (len(l) == 0): # no calls, probably the switch basic block. skip it. 85 | return None 86 | 87 | if (len(l) != 2): 88 | raise Exception("There are %d invoke-virtual calls in this basic block, wtf is this shit?!" % len(l)) # TODO 89 | 90 | if (not l[0].startswith("read")): 91 | raise Exception("The first invoke-virtual call is not a readXXX(), dafuq?") 92 | 93 | typ = l[0][4:].lower() 94 | method = l[1] 95 | field = method[3:] 96 | 97 | if (typ == "message"): 98 | typ = message_type 99 | 100 | if (method.startswith("set")): # optional (or required?) # TODO 101 | return (field, typ, "optional") 102 | 103 | if (method.startswith("add")): # repeated 104 | return (field, typ, "repeated") 105 | 106 | ############################################################## 107 | # Main program starts here 108 | ############################################################## 109 | 110 | if (len(sys.argv) != 2): 111 | print "Usage: %s " % sys.argv[0] 112 | print "Tries to recover the .proto file used by the given APK." 113 | print "Works only with Micro-Protobuf apps, and has only been tested with Google Play." 114 | print "For more information: http://www.segmentationfault.fr/publications/reversing-google-play-and-micro-protobuf-applications/" 115 | print 116 | sys.exit(0) 117 | 118 | apk = APK(sys.argv[1]) 119 | dvm = DalvikVMFormat(apk.get_dex()) 120 | vma = uVMAnalysis(dvm) 121 | 122 | proto_classes = filter(lambda c: "MessageMicro;" in c.get_superclassname(), dvm.get_classes()) 123 | if (len(proto_classes) == 0): 124 | print "Unable to find protobuf micro classes." 125 | sys.exit(0) 126 | 127 | proto_class_names = map(lambda c: c.get_name(), proto_classes) 128 | 129 | """ 130 | cn = proto_class_names[1] 131 | print cn 132 | pprint([(i.split('/')[-1], sorted([(k >> 3) for k in index_basic_blocks(dvm, vma, i).keys()])) for i in proto_class_names]) 133 | """ 134 | 135 | messages_info = {} 136 | for pcn in proto_class_names: 137 | mn = classname_to_messagename(pcn) 138 | d = {} 139 | for (k, bb) in index_basic_blocks(dvm, vma, pcn).items(): 140 | info = analyse_bb(bb, k, pcn) 141 | if (info is not None): 142 | d[k] = info 143 | messages_info[mn] = d 144 | #pprint(messages_info) 145 | 146 | def treeify(seq): 147 | """Resolve message dependencies 148 | http://stackoverflow.com/questions/3464975/how-to-efficiently-merge-multiple-list-of-different-length-into-a-tree-dictonary 149 | """ 150 | ret = {} 151 | for path in seq: 152 | cur = ret 153 | for node in path: 154 | cur = cur.setdefault(node, {}) 155 | return ret 156 | 157 | messages_dep = treeify([k.split('$') for k in messages_info]) 158 | #pprint(messages_dep) 159 | 160 | def print_proto(d, parent = (), indent=0): 161 | """Display all protos""" 162 | for m, sd in sorted(d.items(), cmp=lambda x,y: cmp(x[0],y[0])): 163 | full_name_l = parent+(m,) 164 | full_name = '$'.join(full_name_l) 165 | 166 | is_message_or_group = full_name in messages_info 167 | 168 | if (is_message_or_group): 169 | print_message(m, sd, parent, indent) 170 | else: 171 | print_proto(sd, full_name_l, indent) 172 | 173 | 174 | def print_message(name, sd, parent, indent, title="message", extras=[]): 175 | full_name_l = parent+(name,) 176 | full_name = '$'.join(full_name_l) 177 | 178 | #if (messages_printed[full_name]): # TODO useless 179 | # return False 180 | 181 | # messages_printed[full_name] = True 182 | 183 | if (title == "message"): 184 | print indent*" " + "message %s {" % (name) 185 | else: 186 | print indent*" " + "%s group %s = %d {" % (extras[0], name, extras[1]) 187 | 188 | i = indent+1 189 | infos = messages_info[full_name] 190 | 191 | # Display sub-messages, except groups 192 | groups = [field for (field, typ, _) in infos.values() if typ == 'group'] 193 | print_proto(dict([(k, m) for (k, m) in sd.items() if k not in groups]), full_name_l, i) 194 | 195 | for k, info in sorted(infos.items(), cmp=lambda x,y: cmp(x[0],y[0])): 196 | field, typ, rule = info 197 | 198 | if (typ == 'group'): 199 | print_message(field, sd[field], full_name_l, i, "group", (rule, k)) 200 | else: 201 | print ' '*i + ' '.join([rule, typ.split('$')[-1], ulfirst(field)]) + ' = %d;' % k 202 | 203 | print indent*" " + "}" 204 | 205 | print_proto(messages_dep) 206 | 207 | -------------------------------------------------------------------------------- /apishell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Do not remove 4 | GOOGLE_LOGIN = GOOGLE_PASSWORD = AUTH_TOKEN = None 5 | 6 | BANNER = """ 7 | Google Play Unofficial API Interactive Shell 8 | Successfully logged in using your Google account. The variable 'api' holds the API object. 9 | Feel free to use help(api). 10 | """ 11 | 12 | import sys 13 | import urlparse 14 | import code 15 | from pprint import pprint 16 | from google.protobuf import text_format 17 | 18 | from config import * 19 | from googleplay import GooglePlayAPI 20 | 21 | api = GooglePlayAPI(ANDROID_ID) 22 | api.login(GOOGLE_LOGIN, GOOGLE_PASSWORD, AUTH_TOKEN) 23 | code.interact(BANNER, local=locals()) 24 | -------------------------------------------------------------------------------- /categories.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Do not remove 4 | GOOGLE_LOGIN = GOOGLE_PASSWORD = AUTH_TOKEN = None 5 | 6 | import sys 7 | import urlparse 8 | from pprint import pprint 9 | from google.protobuf import text_format 10 | 11 | from config import * 12 | from googleplay import GooglePlayAPI 13 | 14 | api = GooglePlayAPI(ANDROID_ID) 15 | api.login(GOOGLE_LOGIN, GOOGLE_PASSWORD, AUTH_TOKEN) 16 | response = api.browse() 17 | 18 | print SEPARATOR.join(["ID", "Name"]) 19 | for c in response.category: 20 | print SEPARATOR.join(i.encode('utf8') for i in [urlparse.parse_qs(c.dataUrl)['cat'][0], c.name]) 21 | 22 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # separator used by search.py, categories.py, ... 2 | SEPARATOR = ";" 3 | 4 | LANG = "en_US" # can be en_US, fr_FR, ... 5 | ANDROID_ID = "" # "xxxxxxxxxxxxxxxx" 6 | GOOGLE_LOGIN = "" # "username@gmail.com" 7 | GOOGLE_PASSWORD = "" 8 | AUTH_TOKEN = None # "yyyyyyyyy" 9 | 10 | # Support for rotating among multiple different Google accounts 11 | # Each account must have a corresponding android id, password, and token 12 | ANDROID_ID_S = ["" , "" ] # ["...", "...", ...] 13 | GOOGLE_LOGIN_S = [ "", "" ] # ["user1@gmail.com", "user2@gmail.com" , ...] 14 | GOOGLE_PASSWORD_S = [ "", ""] 15 | AUTH_TOKEN_S = [ None, None ] 16 | 17 | # force the user to edit this file 18 | if any([each == None for each in [ANDROID_ID, GOOGLE_LOGIN, GOOGLE_PASSWORD]]): 19 | raise Exception("config.py not updated") 20 | 21 | # Check we have the same number of accounts and ids 22 | cnt = len(ANDROID_ID_S) 23 | if not(cnt == len(GOOGLE_LOGIN_S) and cnt == len(GOOGLE_PASSWORD_S) and cnt == len(AUTH_TOKEN_S)): 24 | raise Exception("config.py has different number of accounts") 25 | 26 | -------------------------------------------------------------------------------- /dbConfig.py: -------------------------------------------------------------------------------- 1 | #Do not commit login info to github !! 2 | #There is security risk to leak the ip address of our mongodb server 3 | USERNAME = "xxxxx" 4 | PASSWORD = "xxxxxxxxx" 5 | -------------------------------------------------------------------------------- /download.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import traceback 5 | import time 6 | import random 7 | from pprint import pprint 8 | 9 | from googleplay import GooglePlayAPI 10 | from helpers import sizeof_fmt 11 | 12 | from pymongo import MongoClient 13 | from config import * 14 | from dbConfig import USERNAME, PASSWORD 15 | 16 | import datetime 17 | 18 | 19 | # Connect 20 | def connect(android_id, google_login, google_password, auth_token): 21 | api = GooglePlayAPI(android_id) 22 | try: 23 | api.login(google_login, google_password, auth_token) 24 | except: 25 | print >> sys.stderr, int(time.time()) 26 | traceback.print_exc(file=sys.stderr) 27 | return api 28 | 29 | # Get the version code and the offer type from the app details 30 | def downloadApkAndUpdateDB(api, db, packagename, fileDir): 31 | filename = fileDir + '/' + packagename + ".apk" 32 | try: 33 | m = api.details(packagename) 34 | except: 35 | sys.stderr.write("%d \t %s\n" % (int(time.time()), packagename)) 36 | traceback.print_exc(file=sys.stderr) 37 | return (False, traceback.format_exc()) 38 | 39 | doc = m.docV2 40 | vc = doc.details.appDetails.versionCode 41 | 42 | # Zero length offer means the app is not found, possibly removed from the 43 | # play store. 44 | # We treat this as a success case so we can treat this app as processed 45 | # and avoid re-downloading it if we need to recover from an exception. 46 | if (len(doc.offer) == 0): 47 | sys.stdout.write("%d \t %s \t Not Found\n" % (int(time.time()), packagename)) 48 | return (True, packagename + " Not Found") 49 | 50 | try: 51 | ot = doc.offer[0].offerType 52 | except: 53 | sys.stderr.write("%d \t %s\n" % (int(time.time()), packagename)) 54 | print >> sys.stderr, doc 55 | traceback.print_exc(file=sys.stderr) 56 | return (False, traceback.format_exc()) 57 | 58 | 59 | packageName = doc.details.appDetails.packageName 60 | #use eval, since only with api.toDict, pymongo will throw some warning related to wsgi 61 | docDict = eval(str(api.toDict(doc))) 62 | 63 | isApkUpdated = False 64 | #apkDetails is collection for doc + updated timestamp 65 | #if docDict does change, update the db entry 66 | #use more query instead of insertion to speed up 67 | #Do not get fields not in docDict from apkDetails 68 | preDetailsEntry = db.apkDetails.find_one({'details.appDetails.packageName': packageName}, {'updatedTimestamp':0, '_id':0}) 69 | if preDetailsEntry != docDict: 70 | #Warning: sometimes versionCode is not available 71 | #versionCode is used for determine whether apk has been updated 72 | #http://developer.android.com/tools/publishing/versioning.html 73 | try: 74 | isApkUpdated = (not preDetailsEntry) or (docDict['details']['appDetails']['versionCode'] != preDetailsEntry['details']['appDetails']['versionCode']) 75 | except KeyError as e: 76 | isApkUpdated = True 77 | docDict['updatedTimestamp'] = datetime.datetime.utcnow() 78 | db.apkDetails.update({'details.appDetails.packageName': packageName}, docDict, upsert=True) 79 | else: 80 | isApkUpdated = False 81 | 82 | 83 | infoDict = docDict['details']['appDetails'] 84 | 85 | isFree = not doc.offer[0].checkoutFlowRequired 86 | isCurrentVersionDownloaded = False 87 | 88 | # 2015-04-17: Removed apk size restriction. 89 | # According to 90 | # http://android-developers.blogspot.com/2012/03/android-apps-break-50mb-barrier.html 91 | # Each apk file will be at most 50 mb. An app that is listed as bigger than 92 | # 50 mb on the playstore will have expansion packs that can supply 93 | # additional things like graphics, etc. 94 | # http://developer.android.com/distribute/googleplay/publish/preparing.html#size 95 | isSizeExceed = False 96 | # if doc.details.appDetails.installationSize > 52428800: 97 | # isSizeExceed = True 98 | # else: 99 | # isSizeExceed = False 100 | 101 | #Remove db entry fields which are not in infoDict 102 | preInfoEntry = db.apkInfo.find_one({'packageName': packageName}, {'isFree':0, 'isSizeExceed': 0, 'updatedTimestamp':0, '_id':0}) 103 | if preInfoEntry == None: 104 | preInfoEntry = {} 105 | preIsDownloaded = preInfoEntry.pop('isDownloaded', False) 106 | preIsCurrentVersionDownloaded = preInfoEntry.pop('isCurrentVersionDownloaded', False) 107 | preIsApkUpdated = preInfoEntry.pop('isApkUpdated', False) 108 | preFileDir = preInfoEntry.pop('fileDir', '') 109 | 110 | # Default return value 111 | ret = (True, packageName) 112 | 113 | # Download when it is free and not exceed 50 mb and (current version in apkInfo was not downloaded or app has been updated since last time update apkInfo version) 114 | if isFree and (not isSizeExceed) and ((not preIsCurrentVersionDownloaded) or isApkUpdated): 115 | try: 116 | data = api.download(packageName, vc, ot) 117 | except Exception as e: 118 | sys.stderr.write("%d \t %s\n" % (int(time.time()), packageName)) 119 | traceback.print_exc(file=sys.stderr) 120 | isCurrentVersionDownloaded = False 121 | ret = (False, traceback.format_exc()) 122 | else: 123 | print "%d \t %s" % (int(time.time()), packageName) 124 | print "Downloading %s..." % sizeof_fmt(doc.details.appDetails.installationSize), 125 | if preFileDir != '': 126 | fileDir = preFileDir 127 | filename = preFileDir + '/' + packagename + ".apk" 128 | open(filename, "wb").write(data) 129 | print "Done" 130 | isCurrentVersionDownloaded = True 131 | else: 132 | if isApkUpdated == False and preIsCurrentVersionDownloaded == True: 133 | isCurrentVersionDownloaded = True 134 | s = "Escape downloading isFree: %s, isSizeExceed: %s, preIsCurrentVersionDownloaded: %s, isApkUpdated: %s"%( isFree, isSizeExceed, preIsCurrentVersionDownloaded, isApkUpdated) 135 | print "%d \t %s \t %s" % (int(time.time()), packageName, s) 136 | 137 | #update apkInfo entry if infoDict updated (a new entry is also counted as updated) or current version apkDownloaded first time 138 | if preInfoEntry != infoDict or (preIsCurrentVersionDownloaded == False and isCurrentVersionDownloaded == True): 139 | #apkInfo is collection for doc.details.appDetails, and also add isFree and isDownloaded 140 | infoDict['isFree'] = isFree 141 | #infoDict['isDownloaded'] only indicates whether we ever downloaded this apk. 142 | #isCurrentVersionDownloaded indicates whether current version download succeeds 143 | #It is possible previous round of cralwing did download this version, although current round fails to download the same version. 144 | infoDict['isDownloaded'] = preIsDownloaded or isCurrentVersionDownloaded 145 | infoDict['isCurrentVersionDownloaded'] = isCurrentVersionDownloaded 146 | if preFileDir == "" and isCurrentVersionDownloaded: 147 | #Only update fileDir when preFileDir is empty which means never downloaded 148 | #first round crawling has one bug that all apps have a not empty fileDir 149 | infoDict['fileDir'] = fileDir 150 | else: 151 | #Still using previous fileDir 152 | infoDict['fileDir'] = preFileDir 153 | #This is for static analysis purpose. Add the flag when current version apk is sucessfully downloaded and apk version updated or pre version has not been analyzed. 154 | #Everytime only analyze db.apkInfo.find({'isApkUpdated': True}) 155 | #after analyze change the isApkUpdated to False 156 | infoDict['isApkUpdated'] = preIsApkUpdated or (isApkUpdated and isCurrentVersionDownloaded) or ((not isApkUpdated) and (preIsCurrentVersionDownloaded == False) and isCurrentVersionDownloaded == True) 157 | if isSizeExceed != None: 158 | infoDict['isSizeExceed'] = isSizeExceed 159 | infoDict['updatedTimestamp'] = datetime.datetime.utcnow() 160 | #even the download is not successful, if the appDetails is updated, the db entry will be updated 161 | db.apkInfo.update({'packageName': packageName}, infoDict, upsert=True) 162 | 163 | return ret 164 | 165 | if __name__ == '__main__': 166 | if (len(sys.argv) < 2): 167 | print "Usage: %s packagename [directory to store the apk]" 168 | print "Download an app." 169 | sys.exit(0) 170 | 171 | packagename = sys.argv[1] 172 | 173 | if (len(sys.argv) == 3): 174 | fileDir = sys.argv[2] 175 | else: 176 | fileDir = '.' 177 | 178 | client = MongoClient('localhost',27017) 179 | client["admin"].authenticate(USERNAME, PASSWORD) 180 | db = client['androidApp'] 181 | 182 | def apiConnect(): 183 | android_id = ANDROID_ID 184 | google_login = GOOGLE_LOGIN 185 | google_password = GOOGLE_PASSWORD 186 | auth_token = AUTH_TOKEN 187 | 188 | if len(ANDROID_ID_S) > 0: 189 | # Pick a random account to use 190 | i = random.randint(0, len(ANDROID_ID_S) - 1) 191 | android_id = ANDROID_ID_S[i] 192 | google_login = GOOGLE_LOGIN_S[i] 193 | google_password = GOOGLE_PASSWORD_S[i] 194 | auth_token = AUTH_TOKEN_S[i] 195 | 196 | print "\n%d \t Using account %s\n" % (int(time.time()), google_login) 197 | return connect(android_id, google_login, google_password, auth_token) 198 | 199 | api = apiConnect() 200 | downloadApkAndUpdateDB(api, db, packagename, fileDir) 201 | -------------------------------------------------------------------------------- /downloadFromList.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import traceback 4 | from pymongo import MongoClient 5 | from multiprocessing import Pool, get_logger 6 | from download import connect, downloadApkAndUpdateDB 7 | import random 8 | 9 | from config import * 10 | 11 | from dbConfig import USERNAME, PASSWORD 12 | 13 | nextAuthTime = int(time.time()) + random.randint(900,3600) 14 | if __name__ == '__main__': 15 | client = MongoClient('localhost',27017) 16 | client['admin'].authenticate(USERNAME, PASSWORD) 17 | db = client['androidApp'] 18 | 19 | if (len(sys.argv) < 4): 20 | print """Usage: python downloadFromList.py app_list_file file_dir 21 | processed_list_file [download_progress_file]""" 22 | sys.exit(0) 23 | 24 | appListFile = open(sys.argv[1]) 25 | fileDir = sys.argv[2] 26 | processedList = open(sys.argv[3], "a") 27 | progressFile = sys.stdout 28 | 29 | if (len(sys.argv) == 5): 30 | progressFile = open(sys.argv[4], "a") 31 | 32 | #the first line of appList is a timestamp 33 | appListFile.readline() 34 | appList = appListFile.read().split('\n') 35 | appListFile.close() 36 | 37 | numApps = len(appList) 38 | numProcessed = 0 39 | 40 | def apiConnect(): 41 | android_id = ANDROID_ID 42 | google_login = GOOGLE_LOGIN 43 | google_password = GOOGLE_PASSWORD 44 | auth_token = AUTH_TOKEN 45 | 46 | if len(ANDROID_ID_S) > 0: 47 | # Pick a random account to use 48 | i = random.randint(0, len(ANDROID_ID_S) - 1) 49 | android_id = ANDROID_ID_S[i] 50 | google_login = GOOGLE_LOGIN_S[i] 51 | google_password = GOOGLE_PASSWORD_S[i] 52 | auth_token = AUTH_TOKEN_S[i] 53 | 54 | print "\n%d \t Using account %s\n" % (int(time.time()), google_login) 55 | return connect(android_id, google_login, google_password, auth_token) 56 | 57 | api = apiConnect() 58 | nextAuthTime = int(time.time()) + random.randint(900,3600) 59 | def downloadPackage(packagename, db=db, fileDir=fileDir): 60 | global nextAuthTime 61 | global api 62 | global numProcessed 63 | global numApps 64 | try: 65 | if int(time.time()) > nextAuthTime: 66 | api = apiConnect() 67 | nextAuthTime = int(time.time()) + random.randint(900,3600) 68 | 69 | # Update progress 70 | percent = (float(numProcessed)/numApps) * 100 71 | progress = "%d \t %.2f%s" % (int(time.time()), percent, "%") 72 | progressFile.write(progress + "\n") 73 | progressFile.flush() 74 | 75 | success, msg = downloadApkAndUpdateDB(api, db, packagename, fileDir) 76 | 77 | if success: 78 | # Write package name to downloaded list 79 | processedList.write(packagename + "\n") 80 | processedList.flush() 81 | 82 | numProcessed += 1 83 | 84 | # TODO Send email to alert for failure 85 | except: 86 | print >> sys.stderr, int(time.time()), packagename 87 | traceback.print_exc(file=sys.stderr) 88 | return packagename 89 | 90 | for app in appList: 91 | downloadPackage(app) 92 | 93 | processedList.close() 94 | progressFile.close() 95 | 96 | """ 97 | the following code always stops for a period of time 98 | """ 99 | #numberOfProcess = 1 100 | #pool = Pool(numberOfProcess) 101 | #for packagename in pool.imap(downloadPackage, appList): 102 | # print packagename 103 | # #sys.stdout.flush() 104 | -------------------------------------------------------------------------------- /googleplay.proto: -------------------------------------------------------------------------------- 1 | message AckNotificationResponse { 2 | } 3 | message AndroidAppDeliveryData { 4 | optional int64 downloadSize = 1; 5 | optional string signature = 2; 6 | optional string downloadUrl = 3; 7 | repeated AppFileMetadata additionalFile = 4; 8 | repeated HttpCookie downloadAuthCookie = 5; 9 | optional bool forwardLocked = 6; 10 | optional int64 refundTimeout = 7; 11 | optional bool serverInitiated = 8; 12 | optional int64 postInstallRefundWindowMillis = 9; 13 | optional bool immediateStartNeeded = 10; 14 | optional AndroidAppPatchData patchData = 11; 15 | optional EncryptionParams encryptionParams = 12; 16 | } 17 | message AndroidAppPatchData { 18 | optional int32 baseVersionCode = 1; 19 | optional string baseSignature = 2; 20 | optional string downloadUrl = 3; 21 | optional int32 patchFormat = 4; 22 | optional int64 maxPatchSize = 5; 23 | } 24 | message AppFileMetadata { 25 | optional int32 fileType = 1; 26 | optional int32 versionCode = 2; 27 | optional int64 size = 3; 28 | optional string downloadUrl = 4; 29 | } 30 | message EncryptionParams { 31 | optional int32 version = 1; 32 | optional string encryptionKey = 2; 33 | optional string hmacKey = 3; 34 | } 35 | message HttpCookie { 36 | optional string name = 1; 37 | optional string value = 2; 38 | } 39 | message Address { 40 | optional string name = 1; 41 | optional string addressLine1 = 2; 42 | optional string addressLine2 = 3; 43 | optional string city = 4; 44 | optional string state = 5; 45 | optional string postalCode = 6; 46 | optional string postalCountry = 7; 47 | optional string dependentLocality = 8; 48 | optional string sortingCode = 9; 49 | optional string languageCode = 10; 50 | optional string phoneNumber = 11; 51 | optional bool isReduced = 12; 52 | optional string firstName = 13; 53 | optional string lastName = 14; 54 | optional string email = 15; 55 | } 56 | message BookAuthor { 57 | optional string name = 1; 58 | optional string deprecatedQuery = 2; 59 | optional Docid docid = 3; 60 | } 61 | message BookDetails { 62 | repeated BookSubject subject = 3; 63 | optional string publisher = 4; 64 | optional string publicationDate = 5; 65 | optional string isbn = 6; 66 | optional int32 numberOfPages = 7; 67 | optional string subtitle = 8; 68 | repeated BookAuthor author = 9; 69 | optional string readerUrl = 10; 70 | optional string downloadEpubUrl = 11; 71 | optional string downloadPdfUrl = 12; 72 | optional string acsEpubTokenUrl = 13; 73 | optional string acsPdfTokenUrl = 14; 74 | optional bool epubAvailable = 15; 75 | optional bool pdfAvailable = 16; 76 | optional string aboutTheAuthor = 17; 77 | repeated group Identifier = 18 { 78 | optional int32 type = 19; 79 | optional string identifier = 20; 80 | } 81 | } 82 | message BookSubject { 83 | optional string name = 1; 84 | optional string query = 2; 85 | optional string subjectId = 3; 86 | } 87 | message BrowseLink { 88 | optional string name = 1; 89 | optional string dataUrl = 3; 90 | } 91 | message BrowseResponse { 92 | optional string contentsUrl = 1; 93 | optional string promoUrl = 2; 94 | repeated BrowseLink category = 3; 95 | repeated BrowseLink breadcrumb = 4; 96 | } 97 | message AddressChallenge { 98 | optional string responseAddressParam = 1; 99 | optional string responseCheckboxesParam = 2; 100 | optional string title = 3; 101 | optional string descriptionHtml = 4; 102 | repeated FormCheckbox checkbox = 5; 103 | optional Address address = 6; 104 | repeated InputValidationError errorInputField = 7; 105 | optional string errorHtml = 8; 106 | repeated int32 requiredField = 9; 107 | } 108 | message AuthenticationChallenge { 109 | optional int32 authenticationType = 1; 110 | optional string responseAuthenticationTypeParam = 2; 111 | optional string responseRetryCountParam = 3; 112 | optional string pinHeaderText = 4; 113 | optional string pinDescriptionTextHtml = 5; 114 | optional string gaiaHeaderText = 6; 115 | optional string gaiaDescriptionTextHtml = 7; 116 | } 117 | message BuyResponse { 118 | optional PurchaseNotificationResponse purchaseResponse = 1; 119 | optional group CheckoutInfo = 2 { 120 | optional LineItem item = 3; 121 | repeated LineItem subItem = 4; 122 | repeated group CheckoutOption = 5 { 123 | optional string formOfPayment = 6; 124 | optional string encodedAdjustedCart = 7; 125 | optional string instrumentId = 15; 126 | repeated LineItem item = 16; 127 | repeated LineItem subItem = 17; 128 | optional LineItem total = 18; 129 | repeated string footerHtml = 19; 130 | optional int32 instrumentFamily = 29; 131 | repeated int32 deprecatedInstrumentInapplicableReason = 30; 132 | optional bool selectedInstrument = 32; 133 | optional LineItem summary = 33; 134 | repeated string footnoteHtml = 35; 135 | optional Instrument instrument = 43; 136 | optional string purchaseCookie = 45; 137 | repeated string disabledReason = 48; 138 | } 139 | optional string deprecatedCheckoutUrl = 10; 140 | optional string addInstrumentUrl = 11; 141 | repeated string footerHtml = 20; 142 | repeated int32 eligibleInstrumentFamily = 31; 143 | repeated string footnoteHtml = 36; 144 | repeated Instrument eligibleInstrument = 44; 145 | } 146 | optional string continueViaUrl = 8; 147 | optional string purchaseStatusUrl = 9; 148 | optional string checkoutServiceId = 12; 149 | optional bool checkoutTokenRequired = 13; 150 | optional string baseCheckoutUrl = 14; 151 | repeated string tosCheckboxHtml = 37; 152 | optional int32 iabPermissionError = 38; 153 | optional PurchaseStatusResponse purchaseStatusResponse = 39; 154 | optional string purchaseCookie = 46; 155 | optional Challenge challenge = 49; 156 | } 157 | message Challenge { 158 | optional AddressChallenge addressChallenge = 1; 159 | optional AuthenticationChallenge authenticationChallenge = 2; 160 | } 161 | message FormCheckbox { 162 | optional string description = 1; 163 | optional bool checked = 2; 164 | optional bool required = 3; 165 | } 166 | message LineItem { 167 | optional string name = 1; 168 | optional string description = 2; 169 | optional Offer offer = 3; 170 | optional Money amount = 4; 171 | } 172 | message Money { 173 | optional int64 micros = 1; 174 | optional string currencyCode = 2; 175 | optional string formattedAmount = 3; 176 | } 177 | message PurchaseNotificationResponse { 178 | optional int32 status = 1; 179 | optional DebugInfo debugInfo = 2; 180 | optional string localizedErrorMessage = 3; 181 | optional string purchaseId = 4; 182 | } 183 | message PurchaseStatusResponse { 184 | optional int32 status = 1; 185 | optional string statusMsg = 2; 186 | optional string statusTitle = 3; 187 | optional string briefMessage = 4; 188 | optional string infoUrl = 5; 189 | optional LibraryUpdate libraryUpdate = 6; 190 | optional Instrument rejectedInstrument = 7; 191 | optional AndroidAppDeliveryData appDeliveryData = 8; 192 | } 193 | message CheckInstrumentResponse { 194 | optional bool userHasValidInstrument = 1; 195 | optional bool checkoutTokenRequired = 2; 196 | repeated Instrument instrument = 4; 197 | repeated Instrument eligibleInstrument = 5; 198 | } 199 | message UpdateInstrumentRequest { 200 | optional Instrument instrument = 1; 201 | optional string checkoutToken = 2; 202 | } 203 | message UpdateInstrumentResponse { 204 | optional int32 result = 1; 205 | optional string instrumentId = 2; 206 | optional string userMessageHtml = 3; 207 | repeated InputValidationError errorInputField = 4; 208 | optional bool checkoutTokenRequired = 5; 209 | optional RedeemedPromoOffer redeemedOffer = 6; 210 | } 211 | message InitiateAssociationResponse { 212 | optional string userToken = 1; 213 | } 214 | message VerifyAssociationResponse { 215 | optional int32 status = 1; 216 | optional Address billingAddress = 2; 217 | optional CarrierTos carrierTos = 3; 218 | } 219 | message AddCreditCardPromoOffer { 220 | optional string headerText = 1; 221 | optional string descriptionHtml = 2; 222 | optional Image image = 3; 223 | optional string introductoryTextHtml = 4; 224 | optional string offerTitle = 5; 225 | optional string noActionDescription = 6; 226 | optional string termsAndConditionsHtml = 7; 227 | } 228 | message AvailablePromoOffer { 229 | optional AddCreditCardPromoOffer addCreditCardOffer = 1; 230 | } 231 | message CheckPromoOfferResponse { 232 | repeated AvailablePromoOffer availableOffer = 1; 233 | optional RedeemedPromoOffer redeemedOffer = 2; 234 | optional bool checkoutTokenRequired = 3; 235 | } 236 | message RedeemedPromoOffer { 237 | optional string headerText = 1; 238 | optional string descriptionHtml = 2; 239 | optional Image image = 3; 240 | } 241 | message Docid { 242 | optional string backendDocid = 1; 243 | optional int32 type = 2; 244 | optional int32 backend = 3; 245 | } 246 | message Install { 247 | optional fixed64 androidId = 1; 248 | optional int32 version = 2; 249 | optional bool bundled = 3; 250 | } 251 | message Offer { 252 | optional int64 micros = 1; 253 | optional string currencyCode = 2; 254 | optional string formattedAmount = 3; 255 | repeated Offer convertedPrice = 4; 256 | optional bool checkoutFlowRequired = 5; 257 | optional int64 fullPriceMicros = 6; 258 | optional string formattedFullAmount = 7; 259 | optional int32 offerType = 8; 260 | optional RentalTerms rentalTerms = 9; 261 | optional int64 onSaleDate = 10; 262 | repeated string promotionLabel = 11; 263 | optional SubscriptionTerms subscriptionTerms = 12; 264 | optional string formattedName = 13; 265 | optional string formattedDescription = 14; 266 | } 267 | message OwnershipInfo { 268 | optional int64 initiationTimestampMsec = 1; 269 | optional int64 validUntilTimestampMsec = 2; 270 | optional bool autoRenewing = 3; 271 | optional int64 refundTimeoutTimestampMsec = 4; 272 | optional int64 postDeliveryRefundWindowMsec = 5; 273 | } 274 | message RentalTerms { 275 | optional int32 grantPeriodSeconds = 1; 276 | optional int32 activatePeriodSeconds = 2; 277 | } 278 | message SubscriptionTerms { 279 | optional TimePeriod recurringPeriod = 1; 280 | optional TimePeriod trialPeriod = 2; 281 | } 282 | message TimePeriod { 283 | optional int32 unit = 1; 284 | optional int32 count = 2; 285 | } 286 | message BillingAddressSpec { 287 | optional int32 billingAddressType = 1; 288 | repeated int32 requiredField = 2; 289 | } 290 | message CarrierBillingCredentials { 291 | optional string value = 1; 292 | optional int64 expiration = 2; 293 | } 294 | message CarrierBillingInstrument { 295 | optional string instrumentKey = 1; 296 | optional string accountType = 2; 297 | optional string currencyCode = 3; 298 | optional int64 transactionLimit = 4; 299 | optional string subscriberIdentifier = 5; 300 | optional EncryptedSubscriberInfo encryptedSubscriberInfo = 6; 301 | optional CarrierBillingCredentials credentials = 7; 302 | optional CarrierTos acceptedCarrierTos = 8; 303 | } 304 | message CarrierBillingInstrumentStatus { 305 | optional CarrierTos carrierTos = 1; 306 | optional bool associationRequired = 2; 307 | optional bool passwordRequired = 3; 308 | optional PasswordPrompt carrierPasswordPrompt = 4; 309 | optional int32 apiVersion = 5; 310 | optional string name = 6; 311 | } 312 | message CarrierTos { 313 | optional CarrierTosEntry dcbTos = 1; 314 | optional CarrierTosEntry piiTos = 2; 315 | optional bool needsDcbTosAcceptance = 3; 316 | optional bool needsPiiTosAcceptance = 4; 317 | } 318 | message CarrierTosEntry { 319 | optional string url = 1; 320 | optional string version = 2; 321 | } 322 | message CreditCardInstrument { 323 | optional int32 type = 1; 324 | optional string escrowHandle = 2; 325 | optional string lastDigits = 3; 326 | optional int32 expirationMonth = 4; 327 | optional int32 expirationYear = 5; 328 | repeated EfeParam escrowEfeParam = 6; 329 | } 330 | message EfeParam { 331 | optional int32 key = 1; 332 | optional string value = 2; 333 | } 334 | message InputValidationError { 335 | optional int32 inputField = 1; 336 | optional string errorMessage = 2; 337 | } 338 | message Instrument { 339 | optional string instrumentId = 1; 340 | optional Address billingAddress = 2; 341 | optional CreditCardInstrument creditCard = 3; 342 | optional CarrierBillingInstrument carrierBilling = 4; 343 | optional BillingAddressSpec billingAddressSpec = 5; 344 | optional int32 instrumentFamily = 6; 345 | optional CarrierBillingInstrumentStatus carrierBillingStatus = 7; 346 | optional string displayTitle = 8; 347 | } 348 | message PasswordPrompt { 349 | optional string prompt = 1; 350 | optional string forgotPasswordUrl = 2; 351 | } 352 | message ContainerMetadata { 353 | optional string browseUrl = 1; 354 | optional string nextPageUrl = 2; 355 | optional double relevance = 3; 356 | optional int64 estimatedResults = 4; 357 | optional string analyticsCookie = 5; 358 | optional bool ordered = 6; 359 | } 360 | message FlagContentResponse { 361 | } 362 | message DebugInfo { 363 | repeated string message = 1; 364 | repeated group Timing = 2 { 365 | optional string name = 3; 366 | optional double timeInMs = 4; 367 | } 368 | } 369 | message DeliveryResponse { 370 | optional int32 status = 1; 371 | optional AndroidAppDeliveryData appDeliveryData = 2; 372 | } 373 | message BulkDetailsEntry { 374 | optional DocV2 doc = 1; 375 | } 376 | message BulkDetailsRequest { 377 | repeated string docid = 1; 378 | optional bool includeChildDocs = 2; 379 | } 380 | message BulkDetailsResponse { 381 | repeated BulkDetailsEntry entry = 1; 382 | } 383 | message DetailsResponse { 384 | optional DocV1 docV1 = 1; 385 | optional string analyticsCookie = 2; 386 | optional Review userReview = 3; 387 | optional DocV2 docV2 = 4; 388 | optional string footerHtml = 5; 389 | } 390 | message DeviceConfigurationProto { 391 | optional int32 touchScreen = 1; 392 | optional int32 keyboard = 2; 393 | optional int32 navigation = 3; 394 | optional int32 screenLayout = 4; 395 | optional bool hasHardKeyboard = 5; 396 | optional bool hasFiveWayNavigation = 6; 397 | optional int32 screenDensity = 7; 398 | optional int32 glEsVersion = 8; 399 | repeated string systemSharedLibrary = 9; 400 | repeated string systemAvailableFeature = 10; 401 | repeated string nativePlatform = 11; 402 | optional int32 screenWidth = 12; 403 | optional int32 screenHeight = 13; 404 | repeated string systemSupportedLocale = 14; 405 | repeated string glExtension = 15; 406 | optional int32 deviceClass = 16; 407 | optional int32 maxApkDownloadSizeMb = 17; 408 | } 409 | message Document { 410 | optional Docid docid = 1; 411 | optional Docid fetchDocid = 2; 412 | optional Docid sampleDocid = 3; 413 | optional string title = 4; 414 | optional string url = 5; 415 | repeated string snippet = 6; 416 | optional Offer priceDeprecated = 7; 417 | optional Availability availability = 9; 418 | repeated Image image = 10; 419 | repeated Document child = 11; 420 | optional AggregateRating aggregateRating = 13; 421 | repeated Offer offer = 14; 422 | repeated TranslatedText translatedSnippet = 15; 423 | repeated DocumentVariant documentVariant = 16; 424 | repeated string categoryId = 17; 425 | repeated Document decoration = 18; 426 | repeated Document parent = 19; 427 | optional string privacyPolicyUrl = 20; 428 | } 429 | message DocumentVariant { 430 | optional int32 variationType = 1; 431 | optional Rule rule = 2; 432 | optional string title = 3; 433 | repeated string snippet = 4; 434 | optional string recentChanges = 5; 435 | repeated TranslatedText autoTranslation = 6; 436 | repeated Offer offer = 7; 437 | optional int64 channelId = 9; 438 | repeated Document child = 10; 439 | repeated Document decoration = 11; 440 | } 441 | message Image { 442 | optional int32 imageType = 1; 443 | optional group Dimension = 2 { 444 | optional int32 width = 3; 445 | optional int32 height = 4; 446 | } 447 | optional string imageUrl = 5; 448 | optional string altTextLocalized = 6; 449 | optional string secureUrl = 7; 450 | optional int32 positionInSequence = 8; 451 | optional bool supportsFifeUrlOptions = 9; 452 | optional group Citation = 10 { 453 | optional string titleLocalized = 11; 454 | optional string url = 12; 455 | } 456 | } 457 | message TranslatedText { 458 | optional string text = 1; 459 | optional string sourceLocale = 2; 460 | optional string targetLocale = 3; 461 | } 462 | message Badge { 463 | optional string title = 1; 464 | repeated Image image = 2; 465 | optional string browseUrl = 3; 466 | } 467 | message ContainerWithBanner { 468 | optional string colorThemeArgb = 1; 469 | } 470 | message DealOfTheDay { 471 | optional string featuredHeader = 1; 472 | optional string colorThemeArgb = 2; 473 | } 474 | message EditorialSeriesContainer { 475 | optional string seriesTitle = 1; 476 | optional string seriesSubtitle = 2; 477 | optional string episodeTitle = 3; 478 | optional string episodeSubtitle = 4; 479 | optional string colorThemeArgb = 5; 480 | } 481 | message Link { 482 | optional string uri = 1; 483 | } 484 | message PlusOneData { 485 | optional bool setByUser = 1; 486 | optional int64 total = 2; 487 | optional int64 circlesTotal = 3; 488 | repeated PlusPerson circlesPeople = 4; 489 | } 490 | message PlusPerson { 491 | optional string displayName = 2; 492 | optional string profileImageUrl = 4; 493 | } 494 | message PromotedDoc { 495 | optional string title = 1; 496 | optional string subtitle = 2; 497 | repeated Image image = 3; 498 | optional string descriptionHtml = 4; 499 | optional string detailsUrl = 5; 500 | } 501 | message Reason { 502 | optional string briefReason = 1; 503 | optional string detailedReason = 2; 504 | optional string uniqueId = 3; 505 | } 506 | message SectionMetadata { 507 | optional string header = 1; 508 | optional string listUrl = 2; 509 | optional string browseUrl = 3; 510 | optional string descriptionHtml = 4; 511 | } 512 | message SeriesAntenna { 513 | optional string seriesTitle = 1; 514 | optional string seriesSubtitle = 2; 515 | optional string episodeTitle = 3; 516 | optional string episodeSubtitle = 4; 517 | optional string colorThemeArgb = 5; 518 | optional SectionMetadata sectionTracks = 6; 519 | optional SectionMetadata sectionAlbums = 7; 520 | } 521 | message Template { 522 | optional SeriesAntenna seriesAntenna = 1; 523 | optional TileTemplate tileGraphic2X1 = 2; 524 | optional TileTemplate tileGraphic4X2 = 3; 525 | optional TileTemplate tileGraphicColoredTitle2X1 = 4; 526 | optional TileTemplate tileGraphicUpperLeftTitle2X1 = 5; 527 | optional TileTemplate tileDetailsReflectedGraphic2X2 = 6; 528 | optional TileTemplate tileFourBlock4X2 = 7; 529 | optional ContainerWithBanner containerWithBanner = 8; 530 | optional DealOfTheDay dealOfTheDay = 9; 531 | optional TileTemplate tileGraphicColoredTitle4X2 = 10; 532 | optional EditorialSeriesContainer editorialSeriesContainer = 11; 533 | } 534 | message TileTemplate { 535 | optional string colorThemeArgb = 1; 536 | optional string colorTextArgb = 2; 537 | } 538 | message Warning { 539 | optional string localizedMessage = 1; 540 | } 541 | message AlbumDetails { 542 | optional string name = 1; 543 | optional MusicDetails details = 2; 544 | optional ArtistDetails displayArtist = 3; 545 | } 546 | message AppDetails { 547 | optional string developerName = 1; 548 | optional int32 majorVersionNumber = 2; 549 | optional int32 versionCode = 3; 550 | optional string versionString = 4; 551 | optional string title = 5; 552 | repeated string appCategory = 7; 553 | optional int32 contentRating = 8; 554 | optional int64 installationSize = 9; 555 | repeated string permission = 10; 556 | optional string developerEmail = 11; 557 | optional string developerWebsite = 12; 558 | optional string numDownloads = 13; 559 | optional string packageName = 14; 560 | optional string recentChangesHtml = 15; 561 | optional string uploadDate = 16; 562 | repeated FileMetadata file = 17; 563 | optional string appType = 18; 564 | } 565 | message ArtistDetails { 566 | optional string detailsUrl = 1; 567 | optional string name = 2; 568 | optional ArtistExternalLinks externalLinks = 3; 569 | } 570 | message ArtistExternalLinks { 571 | repeated string websiteUrl = 1; 572 | optional string googlePlusProfileUrl = 2; 573 | optional string youtubeChannelUrl = 3; 574 | } 575 | message DocumentDetails { 576 | optional AppDetails appDetails = 1; 577 | optional AlbumDetails albumDetails = 2; 578 | optional ArtistDetails artistDetails = 3; 579 | optional SongDetails songDetails = 4; 580 | optional BookDetails bookDetails = 5; 581 | optional VideoDetails videoDetails = 6; 582 | optional SubscriptionDetails subscriptionDetails = 7; 583 | optional MagazineDetails magazineDetails = 8; 584 | optional TvShowDetails tvShowDetails = 9; 585 | optional TvSeasonDetails tvSeasonDetails = 10; 586 | optional TvEpisodeDetails tvEpisodeDetails = 11; 587 | } 588 | message FileMetadata { 589 | optional int32 fileType = 1; 590 | optional int32 versionCode = 2; 591 | optional int64 size = 3; 592 | } 593 | message MagazineDetails { 594 | optional string parentDetailsUrl = 1; 595 | optional string deviceAvailabilityDescriptionHtml = 2; 596 | optional string psvDescription = 3; 597 | optional string deliveryFrequencyDescription = 4; 598 | } 599 | message MusicDetails { 600 | optional int32 censoring = 1; 601 | optional int32 durationSec = 2; 602 | optional string originalReleaseDate = 3; 603 | optional string label = 4; 604 | repeated ArtistDetails artist = 5; 605 | repeated string genre = 6; 606 | optional string releaseDate = 7; 607 | repeated int32 releaseType = 8; 608 | } 609 | message SongDetails { 610 | optional string name = 1; 611 | optional MusicDetails details = 2; 612 | optional string albumName = 3; 613 | optional int32 trackNumber = 4; 614 | optional string previewUrl = 5; 615 | optional ArtistDetails displayArtist = 6; 616 | } 617 | message SubscriptionDetails { 618 | optional int32 subscriptionPeriod = 1; 619 | } 620 | message Trailer { 621 | optional string trailerId = 1; 622 | optional string title = 2; 623 | optional string thumbnailUrl = 3; 624 | optional string watchUrl = 4; 625 | optional string duration = 5; 626 | } 627 | message TvEpisodeDetails { 628 | optional string parentDetailsUrl = 1; 629 | optional int32 episodeIndex = 2; 630 | optional string releaseDate = 3; 631 | } 632 | message TvSeasonDetails { 633 | optional string parentDetailsUrl = 1; 634 | optional int32 seasonIndex = 2; 635 | optional string releaseDate = 3; 636 | optional string broadcaster = 4; 637 | } 638 | message TvShowDetails { 639 | optional int32 seasonCount = 1; 640 | optional int32 startYear = 2; 641 | optional int32 endYear = 3; 642 | optional string broadcaster = 4; 643 | } 644 | message VideoCredit { 645 | optional int32 creditType = 1; 646 | optional string credit = 2; 647 | repeated string name = 3; 648 | } 649 | message VideoDetails { 650 | repeated VideoCredit credit = 1; 651 | optional string duration = 2; 652 | optional string releaseDate = 3; 653 | optional string contentRating = 4; 654 | optional int64 likes = 5; 655 | optional int64 dislikes = 6; 656 | repeated string genre = 7; 657 | repeated Trailer trailer = 8; 658 | repeated VideoRentalTerm rentalTerm = 9; 659 | } 660 | message VideoRentalTerm { 661 | optional int32 offerType = 1; 662 | optional string offerAbbreviation = 2; 663 | optional string rentalHeader = 3; 664 | repeated group Term = 4 { 665 | optional string header = 5; 666 | optional string body = 6; 667 | } 668 | } 669 | message Bucket { 670 | repeated DocV1 document = 1; 671 | optional bool multiCorpus = 2; 672 | optional string title = 3; 673 | optional string iconUrl = 4; 674 | optional string fullContentsUrl = 5; 675 | optional double relevance = 6; 676 | optional int64 estimatedResults = 7; 677 | optional string analyticsCookie = 8; 678 | optional string fullContentsListUrl = 9; 679 | optional string nextPageUrl = 10; 680 | optional bool ordered = 11; 681 | } 682 | message ListResponse { 683 | repeated Bucket bucket = 1; 684 | repeated DocV2 doc = 2; 685 | } 686 | message DocV1 { 687 | optional Document finskyDoc = 1; 688 | optional string docid = 2; 689 | optional string detailsUrl = 3; 690 | optional string reviewsUrl = 4; 691 | optional string relatedListUrl = 5; 692 | optional string moreByListUrl = 6; 693 | optional string shareUrl = 7; 694 | optional string creator = 8; 695 | optional DocumentDetails details = 9; 696 | optional string descriptionHtml = 10; 697 | optional string relatedBrowseUrl = 11; 698 | optional string moreByBrowseUrl = 12; 699 | optional string relatedHeader = 13; 700 | optional string moreByHeader = 14; 701 | optional string title = 15; 702 | optional PlusOneData plusOneData = 16; 703 | optional string warningMessage = 17; 704 | } 705 | message Annotations { 706 | optional SectionMetadata sectionRelated = 1; 707 | optional SectionMetadata sectionMoreBy = 2; 708 | optional PlusOneData plusOneData = 3; 709 | repeated Warning warning = 4; 710 | optional SectionMetadata sectionBodyOfWork = 5; 711 | optional SectionMetadata sectionCoreContent = 6; 712 | optional Template template = 7; 713 | repeated Badge badgeForCreator = 8; 714 | repeated Badge badgeForDoc = 9; 715 | optional Link link = 10; 716 | optional SectionMetadata sectionCrossSell = 11; 717 | optional SectionMetadata sectionRelatedDocType = 12; 718 | repeated PromotedDoc promotedDoc = 13; 719 | optional string offerNote = 14; 720 | repeated DocV2 subscription = 16; 721 | optional Reason reason = 17; 722 | optional string privacyPolicyUrl = 18; 723 | } 724 | message DocV2 { 725 | optional string docid = 1; 726 | optional string backendDocid = 2; 727 | optional int32 docType = 3; 728 | optional int32 backendId = 4; 729 | optional string title = 5; 730 | optional string creator = 6; 731 | optional string descriptionHtml = 7; 732 | repeated Offer offer = 8; 733 | optional Availability availability = 9; 734 | repeated Image image = 10; 735 | repeated DocV2 child = 11; 736 | optional ContainerMetadata containerMetadata = 12; 737 | optional DocumentDetails details = 13; 738 | optional AggregateRating aggregateRating = 14; 739 | optional Annotations annotations = 15; 740 | optional string detailsUrl = 16; 741 | optional string shareUrl = 17; 742 | optional string reviewsUrl = 18; 743 | optional string backendUrl = 19; 744 | optional string purchaseDetailsUrl = 20; 745 | optional bool detailsReusable = 21; 746 | optional string subtitle = 22; 747 | } 748 | message EncryptedSubscriberInfo { 749 | optional string data = 1; 750 | optional string encryptedKey = 2; 751 | optional string signature = 3; 752 | optional string initVector = 4; 753 | optional int32 googleKeyVersion = 5; 754 | optional int32 carrierKeyVersion = 6; 755 | } 756 | message Availability { 757 | optional int32 restriction = 5; 758 | optional int32 offerType = 6; 759 | optional Rule rule = 7; 760 | repeated group PerDeviceAvailabilityRestriction = 9 { 761 | optional fixed64 androidId = 10; 762 | optional int32 deviceRestriction = 11; 763 | optional int64 channelId = 12; 764 | optional FilterEvaluationInfo filterInfo = 15; 765 | } 766 | optional bool availableIfOwned = 13; 767 | repeated Install install = 14; 768 | optional FilterEvaluationInfo filterInfo = 16; 769 | optional OwnershipInfo ownershipInfo = 17; 770 | } 771 | message FilterEvaluationInfo { 772 | repeated RuleEvaluation ruleEvaluation = 1; 773 | } 774 | message Rule { 775 | optional bool negate = 1; 776 | optional int32 operator = 2; 777 | optional int32 key = 3; 778 | repeated string stringArg = 4; 779 | repeated int64 longArg = 5; 780 | repeated double doubleArg = 6; 781 | repeated Rule subrule = 7; 782 | optional int32 responseCode = 8; 783 | optional string comment = 9; 784 | repeated fixed64 stringArgHash = 10; 785 | repeated int32 constArg = 11; 786 | } 787 | message RuleEvaluation { 788 | optional Rule rule = 1; 789 | repeated string actualStringValue = 2; 790 | repeated int64 actualLongValue = 3; 791 | repeated bool actualBoolValue = 4; 792 | repeated double actualDoubleValue = 5; 793 | } 794 | message LibraryAppDetails { 795 | optional string certificateHash = 2; 796 | optional int64 refundTimeoutTimestampMsec = 3; 797 | optional int64 postDeliveryRefundWindowMsec = 4; 798 | } 799 | message LibraryMutation { 800 | optional Docid docid = 1; 801 | optional int32 offerType = 2; 802 | optional int64 documentHash = 3; 803 | optional bool deleted = 4; 804 | optional LibraryAppDetails appDetails = 5; 805 | optional LibrarySubscriptionDetails subscriptionDetails = 6; 806 | } 807 | message LibrarySubscriptionDetails { 808 | optional int64 initiationTimestampMsec = 1; 809 | optional int64 validUntilTimestampMsec = 2; 810 | optional bool autoRenewing = 3; 811 | optional int64 trialUntilTimestampMsec = 4; 812 | } 813 | message LibraryUpdate { 814 | optional int32 status = 1; 815 | optional int32 corpus = 2; 816 | optional bytes serverToken = 3; 817 | repeated LibraryMutation mutation = 4; 818 | optional bool hasMore = 5; 819 | optional string libraryId = 6; 820 | } 821 | message ClientLibraryState { 822 | optional int32 corpus = 1; 823 | optional bytes serverToken = 2; 824 | optional int64 hashCodeSum = 3; 825 | optional int32 librarySize = 4; 826 | } 827 | message LibraryReplicationRequest { 828 | repeated ClientLibraryState libraryState = 1; 829 | } 830 | message LibraryReplicationResponse { 831 | repeated LibraryUpdate update = 1; 832 | } 833 | message ClickLogEvent { 834 | optional int64 eventTime = 1; 835 | optional string url = 2; 836 | optional string listId = 3; 837 | optional string referrerUrl = 4; 838 | optional string referrerListId = 5; 839 | } 840 | message LogRequest { 841 | repeated ClickLogEvent clickEvent = 1; 842 | } 843 | message LogResponse { 844 | } 845 | message AndroidAppNotificationData { 846 | optional int32 versionCode = 1; 847 | optional string assetId = 2; 848 | } 849 | message InAppNotificationData { 850 | optional string checkoutOrderId = 1; 851 | optional string inAppNotificationId = 2; 852 | } 853 | message LibraryDirtyData { 854 | optional int32 backend = 1; 855 | } 856 | message Notification { 857 | optional int32 notificationType = 1; 858 | optional int64 timestamp = 3; 859 | optional Docid docid = 4; 860 | optional string docTitle = 5; 861 | optional string userEmail = 6; 862 | optional AndroidAppNotificationData appData = 7; 863 | optional AndroidAppDeliveryData appDeliveryData = 8; 864 | optional PurchaseRemovalData purchaseRemovalData = 9; 865 | optional UserNotificationData userNotificationData = 10; 866 | optional InAppNotificationData inAppNotificationData = 11; 867 | optional PurchaseDeclinedData purchaseDeclinedData = 12; 868 | optional string notificationId = 13; 869 | optional LibraryUpdate libraryUpdate = 14; 870 | optional LibraryDirtyData libraryDirtyData = 15; 871 | } 872 | message PurchaseDeclinedData { 873 | optional int32 reason = 1; 874 | optional bool showNotification = 2; 875 | } 876 | message PurchaseRemovalData { 877 | optional bool malicious = 1; 878 | } 879 | message UserNotificationData { 880 | optional string notificationTitle = 1; 881 | optional string notificationText = 2; 882 | optional string tickerText = 3; 883 | optional string dialogTitle = 4; 884 | optional string dialogText = 5; 885 | } 886 | message PlusOneResponse { 887 | } 888 | message RateSuggestedContentResponse { 889 | } 890 | message AggregateRating { 891 | optional int32 type = 1; 892 | optional float starRating = 2; 893 | optional uint64 ratingsCount = 3; 894 | optional uint64 oneStarRatings = 4; 895 | optional uint64 twoStarRatings = 5; 896 | optional uint64 threeStarRatings = 6; 897 | optional uint64 fourStarRatings = 7; 898 | optional uint64 fiveStarRatings = 8; 899 | optional uint64 thumbsUpCount = 9; 900 | optional uint64 thumbsDownCount = 10; 901 | optional uint64 commentCount = 11; 902 | optional double bayesianMeanRating = 12; 903 | } 904 | message DirectPurchase { 905 | optional string detailsUrl = 1; 906 | optional string purchaseDocid = 2; 907 | optional string parentDocid = 3; 908 | optional int32 offerType = 4; 909 | } 910 | message ResolveLinkResponse { 911 | optional string detailsUrl = 1; 912 | optional string browseUrl = 2; 913 | optional string searchUrl = 3; 914 | optional DirectPurchase directPurchase = 4; 915 | optional string homeUrl = 5; 916 | } 917 | message Payload { 918 | optional ListResponse listResponse = 1; 919 | optional DetailsResponse detailsResponse = 2; 920 | optional ReviewResponse reviewResponse = 3; 921 | optional BuyResponse buyResponse = 4; 922 | optional SearchResponse searchResponse = 5; 923 | optional TocResponse tocResponse = 6; 924 | optional BrowseResponse browseResponse = 7; 925 | optional PurchaseStatusResponse purchaseStatusResponse = 8; 926 | optional UpdateInstrumentResponse updateInstrumentResponse = 9; 927 | optional LogResponse logResponse = 10; 928 | optional CheckInstrumentResponse checkInstrumentResponse = 11; 929 | optional PlusOneResponse plusOneResponse = 12; 930 | optional FlagContentResponse flagContentResponse = 13; 931 | optional AckNotificationResponse ackNotificationResponse = 14; 932 | optional InitiateAssociationResponse initiateAssociationResponse = 15; 933 | optional VerifyAssociationResponse verifyAssociationResponse = 16; 934 | optional LibraryReplicationResponse libraryReplicationResponse = 17; 935 | optional RevokeResponse revokeResponse = 18; 936 | optional BulkDetailsResponse bulkDetailsResponse = 19; 937 | optional ResolveLinkResponse resolveLinkResponse = 20; 938 | optional DeliveryResponse deliveryResponse = 21; 939 | optional AcceptTosResponse acceptTosResponse = 22; 940 | optional RateSuggestedContentResponse rateSuggestedContentResponse = 23; 941 | optional CheckPromoOfferResponse checkPromoOfferResponse = 24; 942 | } 943 | message PreFetch { 944 | optional string url = 1; 945 | optional bytes response = 2; 946 | optional string etag = 3; 947 | optional int64 ttl = 4; 948 | optional int64 softTtl = 5; 949 | } 950 | message ResponseWrapper { 951 | optional Payload payload = 1; 952 | optional ServerCommands commands = 2; 953 | repeated PreFetch preFetch = 3; 954 | repeated Notification notification = 4; 955 | } 956 | message ServerCommands { 957 | optional bool clearCache = 1; 958 | optional string displayErrorMessage = 2; 959 | optional string logErrorStacktrace = 3; 960 | } 961 | message GetReviewsResponse { 962 | repeated Review review = 1; 963 | optional int64 matchingCount = 2; 964 | } 965 | message Review { 966 | optional string authorName = 1; 967 | optional string url = 2; 968 | optional string source = 3; 969 | optional string documentVersion = 4; 970 | optional int64 timestampMsec = 5; 971 | optional int32 starRating = 6; 972 | optional string title = 7; 973 | optional string comment = 8; 974 | optional string commentId = 9; 975 | optional string deviceName = 19; 976 | optional string replyText = 29; 977 | optional int64 replyTimestampMsec = 30; 978 | } 979 | message ReviewResponse { 980 | optional GetReviewsResponse getResponse = 1; 981 | optional string nextPageUrl = 2; 982 | } 983 | message RevokeResponse { 984 | optional LibraryUpdate libraryUpdate = 1; 985 | } 986 | message RelatedSearch { 987 | optional string searchUrl = 1; 988 | optional string header = 2; 989 | optional int32 backendId = 3; 990 | optional int32 docType = 4; 991 | optional bool current = 5; 992 | } 993 | message SearchResponse { 994 | optional string originalQuery = 1; 995 | optional string suggestedQuery = 2; 996 | optional bool aggregateQuery = 3; 997 | repeated Bucket bucket = 4; 998 | repeated DocV2 doc = 5; 999 | repeated RelatedSearch relatedSearch = 6; 1000 | } 1001 | message CorpusMetadata { 1002 | optional int32 backend = 1; 1003 | optional string name = 2; 1004 | optional string landingUrl = 3; 1005 | optional string libraryName = 4; 1006 | } 1007 | message Experiments { 1008 | repeated string experimentId = 1; 1009 | } 1010 | message TocResponse { 1011 | repeated CorpusMetadata corpus = 1; 1012 | optional int32 tosVersionDeprecated = 2; 1013 | optional string tosContent = 3; 1014 | optional string homeUrl = 4; 1015 | optional Experiments experiments = 5; 1016 | optional string tosCheckboxTextMarketingEmails = 6; 1017 | optional string tosToken = 7; 1018 | optional UserSettings userSettings = 8; 1019 | optional string iconOverrideUrl = 9; 1020 | } 1021 | message UserSettings { 1022 | optional bool tosCheckboxMarketingEmailsOptedIn = 1; 1023 | } 1024 | message AcceptTosResponse { 1025 | } 1026 | message AckNotificationsRequestProto { 1027 | repeated string notificationId = 1; 1028 | optional SignatureHashProto signatureHash = 2; 1029 | repeated string nackNotificationId = 3; 1030 | } 1031 | message AckNotificationsResponseProto { 1032 | } 1033 | message AddressProto { 1034 | optional string address1 = 1; 1035 | optional string address2 = 2; 1036 | optional string city = 3; 1037 | optional string state = 4; 1038 | optional string postalCode = 5; 1039 | optional string country = 6; 1040 | optional string name = 7; 1041 | optional string type = 8; 1042 | optional string phone = 9; 1043 | } 1044 | message AppDataProto { 1045 | optional string key = 1; 1046 | optional string value = 2; 1047 | } 1048 | message AppSuggestionProto { 1049 | optional ExternalAssetProto assetInfo = 1; 1050 | } 1051 | message AssetIdentifierProto { 1052 | optional string packageName = 1; 1053 | optional int32 versionCode = 2; 1054 | optional string assetId = 3; 1055 | } 1056 | message AssetsRequestProto { 1057 | optional int32 assetType = 1; 1058 | optional string query = 2; 1059 | optional string categoryId = 3; 1060 | repeated string assetId = 4; 1061 | optional bool retrieveVendingHistory = 5; 1062 | optional bool retrieveExtendedInfo = 6; 1063 | optional int32 sortOrder = 7; 1064 | optional int64 startIndex = 8; 1065 | optional int64 numEntries = 9; 1066 | optional int32 viewFilter = 10; 1067 | optional string rankingType = 11; 1068 | optional bool retrieveCarrierChannel = 12; 1069 | repeated string pendingDownloadAssetId = 13; 1070 | optional bool reconstructVendingHistory = 14; 1071 | optional bool unfilteredResults = 15; 1072 | repeated string badgeId = 16; 1073 | } 1074 | message AssetsResponseProto { 1075 | repeated ExternalAssetProto asset = 1; 1076 | optional int64 numTotalEntries = 2; 1077 | optional string correctedQuery = 3; 1078 | repeated ExternalAssetProto altAsset = 4; 1079 | optional int64 numCorrectedEntries = 5; 1080 | optional string header = 6; 1081 | optional int32 listType = 7; 1082 | } 1083 | message BillingEventRequestProto { 1084 | optional int32 eventType = 1; 1085 | optional string billingParametersId = 2; 1086 | optional bool resultSuccess = 3; 1087 | optional string clientMessage = 4; 1088 | optional ExternalCarrierBillingInstrumentProto carrierInstrument = 5; 1089 | } 1090 | message BillingEventResponseProto { 1091 | } 1092 | message BillingParameterProto { 1093 | optional string id = 1; 1094 | optional string name = 2; 1095 | repeated string mncMcc = 3; 1096 | repeated string backendUrl = 4; 1097 | optional string iconId = 5; 1098 | optional int32 billingInstrumentType = 6; 1099 | optional string applicationId = 7; 1100 | optional string tosUrl = 8; 1101 | optional bool instrumentTosRequired = 9; 1102 | optional int32 apiVersion = 10; 1103 | optional bool perTransactionCredentialsRequired = 11; 1104 | optional bool sendSubscriberIdWithCarrierBillingRequests = 12; 1105 | optional int32 deviceAssociationMethod = 13; 1106 | optional string userTokenRequestMessage = 14; 1107 | optional string userTokenRequestAddress = 15; 1108 | optional bool passphraseRequired = 16; 1109 | } 1110 | message CarrierBillingCredentialsProto { 1111 | optional string credentials = 1; 1112 | optional int64 credentialsTimeout = 2; 1113 | } 1114 | message CategoryProto { 1115 | optional int32 assetType = 2; 1116 | optional string categoryId = 3; 1117 | optional string categoryDisplay = 4; 1118 | optional string categorySubtitle = 5; 1119 | repeated string promotedAssetsNew = 6; 1120 | repeated string promotedAssetsHome = 7; 1121 | repeated CategoryProto subCategories = 8; 1122 | repeated string promotedAssetsPaid = 9; 1123 | repeated string promotedAssetsFree = 10; 1124 | } 1125 | message CheckForNotificationsRequestProto { 1126 | optional int64 alarmDuration = 1; 1127 | } 1128 | message CheckForNotificationsResponseProto { 1129 | } 1130 | message CheckLicenseRequestProto { 1131 | optional string packageName = 1; 1132 | optional int32 versionCode = 2; 1133 | optional int64 nonce = 3; 1134 | } 1135 | message CheckLicenseResponseProto { 1136 | optional int32 responseCode = 1; 1137 | optional string signedData = 2; 1138 | optional string signature = 3; 1139 | } 1140 | message CommentsRequestProto { 1141 | optional string assetId = 1; 1142 | optional int64 startIndex = 2; 1143 | optional int64 numEntries = 3; 1144 | optional bool shouldReturnSelfComment = 4; 1145 | optional string assetReferrer = 5; 1146 | } 1147 | message CommentsResponseProto { 1148 | repeated ExternalCommentProto comment = 1; 1149 | optional int64 numTotalEntries = 2; 1150 | optional ExternalCommentProto selfComment = 3; 1151 | } 1152 | message ContentSyncRequestProto { 1153 | optional bool incremental = 1; 1154 | repeated group AssetInstallState = 2 { 1155 | optional string assetId = 3; 1156 | optional int32 assetState = 4; 1157 | optional int64 installTime = 5; 1158 | optional int64 uninstallTime = 6; 1159 | optional string packageName = 7; 1160 | optional int32 versionCode = 8; 1161 | optional string assetReferrer = 9; 1162 | } 1163 | repeated group SystemApp = 10 { 1164 | optional string packageName = 11; 1165 | optional int32 versionCode = 12; 1166 | repeated string certificateHash = 13; 1167 | } 1168 | optional int32 sideloadedAppCount = 14; 1169 | } 1170 | message ContentSyncResponseProto { 1171 | optional int32 numUpdatesAvailable = 1; 1172 | } 1173 | message DataMessageProto { 1174 | optional string category = 1; 1175 | repeated AppDataProto appData = 3; 1176 | } 1177 | message DownloadInfoProto { 1178 | optional int64 apkSize = 1; 1179 | repeated FileMetadataProto additionalFile = 2; 1180 | } 1181 | message ExternalAssetProto { 1182 | optional string id = 1; 1183 | optional string title = 2; 1184 | optional int32 assetType = 3; 1185 | optional string owner = 4; 1186 | optional string version = 5; 1187 | optional string price = 6; 1188 | optional string averageRating = 7; 1189 | optional int64 numRatings = 8; 1190 | optional group PurchaseInformation = 9 { 1191 | optional int64 purchaseTime = 10; 1192 | optional int64 refundTimeoutTime = 11; 1193 | optional int32 refundStartPolicy = 45; 1194 | optional int64 refundWindowDuration = 46; 1195 | } 1196 | optional group ExtendedInfo = 12 { 1197 | optional string description = 13; 1198 | optional int64 downloadCount = 14; 1199 | repeated string applicationPermissionId = 15; 1200 | optional int64 requiredInstallationSize = 16; 1201 | optional string packageName = 17; 1202 | optional string category = 18; 1203 | optional bool forwardLocked = 19; 1204 | optional string contactEmail = 20; 1205 | optional bool everInstalledByUser = 21; 1206 | optional string downloadCountString = 23; 1207 | optional string contactPhone = 26; 1208 | optional string contactWebsite = 27; 1209 | optional bool nextPurchaseRefundable = 28; 1210 | optional int32 numScreenshots = 30; 1211 | optional string promotionalDescription = 31; 1212 | optional int32 serverAssetState = 34; 1213 | optional int32 contentRatingLevel = 36; 1214 | optional string contentRatingString = 37; 1215 | optional string recentChanges = 38; 1216 | repeated group PackageDependency = 39 { 1217 | optional string packageName = 41; 1218 | optional bool skipPermissions = 42; 1219 | } 1220 | optional string videoLink = 43; 1221 | optional DownloadInfoProto downloadInfo = 49; 1222 | } 1223 | optional string ownerId = 22; 1224 | optional string packageName = 24; 1225 | optional int32 versionCode = 25; 1226 | optional bool bundledAsset = 29; 1227 | optional string priceCurrency = 32; 1228 | optional int64 priceMicros = 33; 1229 | optional string filterReason = 35; 1230 | optional string actualSellerPrice = 40; 1231 | repeated ExternalBadgeProto appBadge = 47; 1232 | repeated ExternalBadgeProto ownerBadge = 48; 1233 | } 1234 | message ExternalBadgeImageProto { 1235 | optional int32 usage = 1; 1236 | optional string url = 2; 1237 | } 1238 | message ExternalBadgeProto { 1239 | optional string localizedTitle = 1; 1240 | optional string localizedDescription = 2; 1241 | repeated ExternalBadgeImageProto badgeImage = 3; 1242 | optional string searchId = 4; 1243 | } 1244 | message ExternalCarrierBillingInstrumentProto { 1245 | optional string instrumentKey = 1; 1246 | optional string subscriberIdentifier = 2; 1247 | optional string accountType = 3; 1248 | optional string subscriberCurrency = 4; 1249 | optional uint64 transactionLimit = 5; 1250 | optional string subscriberName = 6; 1251 | optional string address1 = 7; 1252 | optional string address2 = 8; 1253 | optional string city = 9; 1254 | optional string state = 10; 1255 | optional string postalCode = 11; 1256 | optional string country = 12; 1257 | optional EncryptedSubscriberInfo encryptedSubscriberInfo = 13; 1258 | } 1259 | message ExternalCommentProto { 1260 | optional string body = 1; 1261 | optional int32 rating = 2; 1262 | optional string creatorName = 3; 1263 | optional int64 creationTime = 4; 1264 | optional string creatorId = 5; 1265 | } 1266 | message ExternalCreditCard { 1267 | optional string type = 1; 1268 | optional string lastDigits = 2; 1269 | optional int32 expYear = 3; 1270 | optional int32 expMonth = 4; 1271 | optional string personName = 5; 1272 | optional string countryCode = 6; 1273 | optional string postalCode = 7; 1274 | optional bool makeDefault = 8; 1275 | optional string address1 = 9; 1276 | optional string address2 = 10; 1277 | optional string city = 11; 1278 | optional string state = 12; 1279 | optional string phone = 13; 1280 | } 1281 | message ExternalPaypalInstrumentProto { 1282 | optional string instrumentKey = 1; 1283 | optional string preapprovalKey = 2; 1284 | optional string paypalEmail = 3; 1285 | optional AddressProto paypalAddress = 4; 1286 | optional bool multiplePaypalInstrumentsSupported = 5; 1287 | } 1288 | message FileMetadataProto { 1289 | optional int32 fileType = 1; 1290 | optional int32 versionCode = 2; 1291 | optional int64 size = 3; 1292 | optional string downloadUrl = 4; 1293 | } 1294 | message GetAddressSnippetRequestProto { 1295 | optional EncryptedSubscriberInfo encryptedSubscriberInfo = 1; 1296 | } 1297 | message GetAddressSnippetResponseProto { 1298 | optional string addressSnippet = 1; 1299 | } 1300 | message GetAssetRequestProto { 1301 | optional string assetId = 1; 1302 | optional string directDownloadKey = 2; 1303 | } 1304 | message GetAssetResponseProto { 1305 | optional group InstallAsset = 1 { 1306 | optional string assetId = 2; 1307 | optional string assetName = 3; 1308 | optional string assetType = 4; 1309 | optional string assetPackage = 5; 1310 | optional string blobUrl = 6; 1311 | optional string assetSignature = 7; 1312 | optional int64 assetSize = 8; 1313 | optional int64 refundTimeoutMillis = 9; 1314 | optional bool forwardLocked = 10; 1315 | optional bool secured = 11; 1316 | optional int32 versionCode = 12; 1317 | optional string downloadAuthCookieName = 13; 1318 | optional string downloadAuthCookieValue = 14; 1319 | optional int64 postInstallRefundWindowMillis = 16; 1320 | } 1321 | repeated FileMetadataProto additionalFile = 15; 1322 | } 1323 | message GetCarrierInfoRequestProto { 1324 | } 1325 | message GetCarrierInfoResponseProto { 1326 | optional bool carrierChannelEnabled = 1; 1327 | optional bytes carrierLogoIcon = 2; 1328 | optional bytes carrierBanner = 3; 1329 | optional string carrierSubtitle = 4; 1330 | optional string carrierTitle = 5; 1331 | optional int32 carrierImageDensity = 6; 1332 | } 1333 | message GetCategoriesRequestProto { 1334 | optional bool prefetchPromoData = 1; 1335 | } 1336 | message GetCategoriesResponseProto { 1337 | repeated CategoryProto categories = 1; 1338 | } 1339 | message GetImageRequestProto { 1340 | optional string assetId = 1; 1341 | optional int32 imageUsage = 3; 1342 | optional string imageId = 4; 1343 | optional int32 screenPropertyWidth = 5; 1344 | optional int32 screenPropertyHeight = 6; 1345 | optional int32 screenPropertyDensity = 7; 1346 | optional int32 productType = 8; 1347 | } 1348 | message GetImageResponseProto { 1349 | optional bytes imageData = 1; 1350 | optional int32 imageDensity = 2; 1351 | } 1352 | message GetMarketMetadataRequestProto { 1353 | optional int64 lastRequestTime = 1; 1354 | optional DeviceConfigurationProto deviceConfiguration = 2; 1355 | optional bool deviceRoaming = 3; 1356 | repeated string marketSignatureHash = 4; 1357 | optional int32 contentRating = 5; 1358 | optional string deviceModelName = 6; 1359 | optional string deviceManufacturerName = 7; 1360 | } 1361 | message GetMarketMetadataResponseProto { 1362 | optional int32 latestClientVersionCode = 1; 1363 | optional string latestClientUrl = 2; 1364 | optional bool paidAppsEnabled = 3; 1365 | repeated BillingParameterProto billingParameter = 4; 1366 | optional bool commentPostEnabled = 5; 1367 | optional bool billingEventsEnabled = 6; 1368 | optional string warningMessage = 7; 1369 | optional bool inAppBillingEnabled = 8; 1370 | optional int32 inAppBillingMaxApiVersion = 9; 1371 | } 1372 | message GetSubCategoriesRequestProto { 1373 | optional int32 assetType = 1; 1374 | } 1375 | message GetSubCategoriesResponseProto { 1376 | repeated group SubCategory = 1 { 1377 | optional string subCategoryDisplay = 2; 1378 | optional string subCategoryId = 3; 1379 | } 1380 | } 1381 | message InAppPurchaseInformationRequestProto { 1382 | optional SignatureHashProto signatureHash = 1; 1383 | optional int64 nonce = 2; 1384 | repeated string notificationId = 3; 1385 | optional string signatureAlgorithm = 4; 1386 | optional int32 billingApiVersion = 5; 1387 | } 1388 | message InAppPurchaseInformationResponseProto { 1389 | optional SignedDataProto signedResponse = 1; 1390 | repeated StatusBarNotificationProto statusBarNotification = 2; 1391 | optional PurchaseResultProto purchaseResult = 3; 1392 | } 1393 | message InAppRestoreTransactionsRequestProto { 1394 | optional SignatureHashProto signatureHash = 1; 1395 | optional int64 nonce = 2; 1396 | optional string signatureAlgorithm = 3; 1397 | optional int32 billingApiVersion = 4; 1398 | } 1399 | message InAppRestoreTransactionsResponseProto { 1400 | optional SignedDataProto signedResponse = 1; 1401 | optional PurchaseResultProto purchaseResult = 2; 1402 | } 1403 | /* 1404 | message InputValidationError { 1405 | optional int32 inputField = 1; 1406 | optional string errorMessage = 2; 1407 | } 1408 | */ 1409 | message ModifyCommentRequestProto { 1410 | optional string assetId = 1; 1411 | optional ExternalCommentProto comment = 2; 1412 | optional bool deleteComment = 3; 1413 | optional bool flagAsset = 4; 1414 | optional int32 flagType = 5; 1415 | optional string flagMessage = 6; 1416 | optional bool nonFlagFlow = 7; 1417 | } 1418 | message ModifyCommentResponseProto { 1419 | } 1420 | message PaypalCountryInfoProto { 1421 | optional bool birthDateRequired = 1; 1422 | optional string tosText = 2; 1423 | optional string billingAgreementText = 3; 1424 | optional string preTosText = 4; 1425 | } 1426 | message PaypalCreateAccountRequestProto { 1427 | optional string firstName = 1; 1428 | optional string lastName = 2; 1429 | optional AddressProto address = 3; 1430 | optional string birthDate = 4; 1431 | } 1432 | message PaypalCreateAccountResponseProto { 1433 | optional string createAccountKey = 1; 1434 | } 1435 | message PaypalCredentialsProto { 1436 | optional string preapprovalKey = 1; 1437 | optional string paypalEmail = 2; 1438 | } 1439 | message PaypalMassageAddressRequestProto { 1440 | optional AddressProto address = 1; 1441 | } 1442 | message PaypalMassageAddressResponseProto { 1443 | optional AddressProto address = 1; 1444 | } 1445 | message PaypalPreapprovalCredentialsRequestProto { 1446 | optional string gaiaAuthToken = 1; 1447 | optional string billingInstrumentId = 2; 1448 | } 1449 | message PaypalPreapprovalCredentialsResponseProto { 1450 | optional int32 resultCode = 1; 1451 | optional string paypalAccountKey = 2; 1452 | optional string paypalEmail = 3; 1453 | } 1454 | message PaypalPreapprovalDetailsRequestProto { 1455 | optional bool getAddress = 1; 1456 | optional string preapprovalKey = 2; 1457 | } 1458 | message PaypalPreapprovalDetailsResponseProto { 1459 | optional string paypalEmail = 1; 1460 | optional AddressProto address = 2; 1461 | } 1462 | message PaypalPreapprovalRequestProto { 1463 | } 1464 | message PaypalPreapprovalResponseProto { 1465 | optional string preapprovalKey = 1; 1466 | } 1467 | message PendingNotificationsProto { 1468 | repeated DataMessageProto notification = 1; 1469 | optional int64 nextCheckMillis = 2; 1470 | } 1471 | message PrefetchedBundleProto { 1472 | optional SingleRequestProto request = 1; 1473 | optional SingleResponseProto response = 2; 1474 | } 1475 | message PurchaseCartInfoProto { 1476 | optional string itemPrice = 1; 1477 | optional string taxInclusive = 2; 1478 | optional string taxExclusive = 3; 1479 | optional string total = 4; 1480 | optional string taxMessage = 5; 1481 | optional string footerMessage = 6; 1482 | optional string priceCurrency = 7; 1483 | optional int64 priceMicros = 8; 1484 | } 1485 | message PurchaseInfoProto { 1486 | optional string transactionId = 1; 1487 | optional PurchaseCartInfoProto cartInfo = 2; 1488 | optional group BillingInstruments = 3 { 1489 | repeated group BillingInstrument = 4 { 1490 | optional string id = 5; 1491 | optional string name = 6; 1492 | optional bool isInvalid = 7; 1493 | optional int32 instrumentType = 11; 1494 | optional int32 instrumentStatus = 14; 1495 | } 1496 | optional string defaultBillingInstrumentId = 8; 1497 | } 1498 | repeated int32 errorInputFields = 9; 1499 | optional string refundPolicy = 10; 1500 | optional bool userCanAddGdd = 12; 1501 | repeated int32 eligibleInstrumentTypes = 13; 1502 | optional string orderId = 15; 1503 | } 1504 | message PurchaseMetadataRequestProto { 1505 | optional bool deprecatedRetrieveBillingCountries = 1; 1506 | optional int32 billingInstrumentType = 2; 1507 | } 1508 | message PurchaseMetadataResponseProto { 1509 | optional group Countries = 1 { 1510 | repeated group Country = 2 { 1511 | optional string countryCode = 3; 1512 | optional string countryName = 4; 1513 | optional PaypalCountryInfoProto paypalCountryInfo = 5; 1514 | optional bool allowsReducedBillingAddress = 6; 1515 | repeated group InstrumentAddressSpec = 7 { 1516 | optional int32 instrumentFamily = 8; 1517 | optional BillingAddressSpec billingAddressSpec = 9; 1518 | } 1519 | } 1520 | } 1521 | } 1522 | message PurchaseOrderRequestProto { 1523 | optional string gaiaAuthToken = 1; 1524 | optional string assetId = 2; 1525 | optional string transactionId = 3; 1526 | optional string billingInstrumentId = 4; 1527 | optional bool tosAccepted = 5; 1528 | optional CarrierBillingCredentialsProto carrierBillingCredentials = 6; 1529 | optional string existingOrderId = 7; 1530 | optional int32 billingInstrumentType = 8; 1531 | optional string billingParametersId = 9; 1532 | optional PaypalCredentialsProto paypalCredentials = 10; 1533 | optional RiskHeaderInfoProto riskHeaderInfo = 11; 1534 | optional int32 productType = 12; 1535 | optional SignatureHashProto signatureHash = 13; 1536 | optional string developerPayload = 14; 1537 | } 1538 | message PurchaseOrderResponseProto { 1539 | optional int32 deprecatedResultCode = 1; 1540 | optional PurchaseInfoProto purchaseInfo = 2; 1541 | optional ExternalAssetProto asset = 3; 1542 | optional PurchaseResultProto purchaseResult = 4; 1543 | } 1544 | message PurchasePostRequestProto { 1545 | optional string gaiaAuthToken = 1; 1546 | optional string assetId = 2; 1547 | optional string transactionId = 3; 1548 | optional group BillingInstrumentInfo = 4 { 1549 | optional string billingInstrumentId = 5; 1550 | optional ExternalCreditCard creditCard = 6; 1551 | optional ExternalCarrierBillingInstrumentProto carrierInstrument = 9; 1552 | optional ExternalPaypalInstrumentProto paypalInstrument = 10; 1553 | } 1554 | optional bool tosAccepted = 7; 1555 | optional string cbInstrumentKey = 8; 1556 | optional bool paypalAuthConfirmed = 11; 1557 | optional int32 productType = 12; 1558 | optional SignatureHashProto signatureHash = 13; 1559 | } 1560 | message PurchasePostResponseProto { 1561 | optional int32 deprecatedResultCode = 1; 1562 | optional PurchaseInfoProto purchaseInfo = 2; 1563 | optional string termsOfServiceUrl = 3; 1564 | optional string termsOfServiceText = 4; 1565 | optional string termsOfServiceName = 5; 1566 | optional string termsOfServiceCheckboxText = 6; 1567 | optional string termsOfServiceHeaderText = 7; 1568 | optional PurchaseResultProto purchaseResult = 8; 1569 | } 1570 | message PurchaseProductRequestProto { 1571 | optional int32 productType = 1; 1572 | optional string productId = 2; 1573 | optional SignatureHashProto signatureHash = 3; 1574 | } 1575 | message PurchaseProductResponseProto { 1576 | optional string title = 1; 1577 | optional string itemTitle = 2; 1578 | optional string itemDescription = 3; 1579 | optional string merchantField = 4; 1580 | } 1581 | message PurchaseResultProto { 1582 | optional int32 resultCode = 1; 1583 | optional string resultCodeMessage = 2; 1584 | } 1585 | message QuerySuggestionProto { 1586 | optional string query = 1; 1587 | optional int32 estimatedNumResults = 2; 1588 | optional int32 queryWeight = 3; 1589 | } 1590 | message QuerySuggestionRequestProto { 1591 | optional string query = 1; 1592 | optional int32 requestType = 2; 1593 | } 1594 | message QuerySuggestionResponseProto { 1595 | repeated group Suggestion = 1 { 1596 | optional AppSuggestionProto appSuggestion = 2; 1597 | optional QuerySuggestionProto querySuggestion = 3; 1598 | } 1599 | optional int32 estimatedNumAppSuggestions = 4; 1600 | optional int32 estimatedNumQuerySuggestions = 5; 1601 | } 1602 | message RateCommentRequestProto { 1603 | optional string assetId = 1; 1604 | optional string creatorId = 2; 1605 | optional int32 commentRating = 3; 1606 | } 1607 | message RateCommentResponseProto { 1608 | } 1609 | message ReconstructDatabaseRequestProto { 1610 | optional bool retrieveFullHistory = 1; 1611 | } 1612 | message ReconstructDatabaseResponseProto { 1613 | repeated AssetIdentifierProto asset = 1; 1614 | } 1615 | message RefundRequestProto { 1616 | optional string assetId = 1; 1617 | } 1618 | message RefundResponseProto { 1619 | optional int32 result = 1; 1620 | optional ExternalAssetProto asset = 2; 1621 | optional string resultDetail = 3; 1622 | } 1623 | message RemoveAssetRequestProto { 1624 | optional string assetId = 1; 1625 | } 1626 | message RequestPropertiesProto { 1627 | optional string userAuthToken = 1; 1628 | optional bool userAuthTokenSecure = 2; 1629 | optional int32 softwareVersion = 3; 1630 | optional string aid = 4; 1631 | optional string productNameAndVersion = 5; 1632 | optional string userLanguage = 6; 1633 | optional string userCountry = 7; 1634 | optional string operatorName = 8; 1635 | optional string simOperatorName = 9; 1636 | optional string operatorNumericName = 10; 1637 | optional string simOperatorNumericName = 11; 1638 | optional string clientId = 12; 1639 | optional string loggingId = 13; 1640 | } 1641 | message RequestProto { 1642 | optional RequestPropertiesProto requestProperties = 1; 1643 | repeated group Request = 2 { 1644 | optional RequestSpecificPropertiesProto requestSpecificProperties = 3; 1645 | optional AssetsRequestProto assetRequest = 4; 1646 | optional CommentsRequestProto commentsRequest = 5; 1647 | optional ModifyCommentRequestProto modifyCommentRequest = 6; 1648 | optional PurchasePostRequestProto purchasePostRequest = 7; 1649 | optional PurchaseOrderRequestProto purchaseOrderRequest = 8; 1650 | optional ContentSyncRequestProto contentSyncRequest = 9; 1651 | optional GetAssetRequestProto getAssetRequest = 10; 1652 | optional GetImageRequestProto getImageRequest = 11; 1653 | optional RefundRequestProto refundRequest = 12; 1654 | optional PurchaseMetadataRequestProto purchaseMetadataRequest = 13; 1655 | optional GetSubCategoriesRequestProto subCategoriesRequest = 14; 1656 | optional UninstallReasonRequestProto uninstallReasonRequest = 16; 1657 | optional RateCommentRequestProto rateCommentRequest = 17; 1658 | optional CheckLicenseRequestProto checkLicenseRequest = 18; 1659 | optional GetMarketMetadataRequestProto getMarketMetadataRequest = 19; 1660 | optional GetCategoriesRequestProto getCategoriesRequest = 21; 1661 | optional GetCarrierInfoRequestProto getCarrierInfoRequest = 22; 1662 | optional RemoveAssetRequestProto removeAssetRequest = 23; 1663 | optional RestoreApplicationsRequestProto restoreApplicationsRequest = 24; 1664 | optional QuerySuggestionRequestProto querySuggestionRequest = 25; 1665 | optional BillingEventRequestProto billingEventRequest = 26; 1666 | optional PaypalPreapprovalRequestProto paypalPreapprovalRequest = 27; 1667 | optional PaypalPreapprovalDetailsRequestProto paypalPreapprovalDetailsRequest = 28; 1668 | optional PaypalCreateAccountRequestProto paypalCreateAccountRequest = 29; 1669 | optional PaypalPreapprovalCredentialsRequestProto paypalPreapprovalCredentialsRequest = 30; 1670 | optional InAppRestoreTransactionsRequestProto inAppRestoreTransactionsRequest = 31; 1671 | optional InAppPurchaseInformationRequestProto inAppPurchaseInformationRequest = 32; 1672 | optional CheckForNotificationsRequestProto checkForNotificationsRequest = 33; 1673 | optional AckNotificationsRequestProto ackNotificationsRequest = 34; 1674 | optional PurchaseProductRequestProto purchaseProductRequest = 35; 1675 | optional ReconstructDatabaseRequestProto reconstructDatabaseRequest = 36; 1676 | optional PaypalMassageAddressRequestProto paypalMassageAddressRequest = 37; 1677 | optional GetAddressSnippetRequestProto getAddressSnippetRequest = 38; 1678 | } 1679 | } 1680 | message RequestSpecificPropertiesProto { 1681 | optional string ifNoneMatch = 1; 1682 | } 1683 | message ResponsePropertiesProto { 1684 | optional int32 result = 1; 1685 | optional int32 maxAge = 2; 1686 | optional string etag = 3; 1687 | optional int32 serverVersion = 4; 1688 | optional int32 maxAgeConsumable = 6; 1689 | optional string errorMessage = 7; 1690 | repeated InputValidationError errorInputField = 8; 1691 | } 1692 | message ResponseProto { 1693 | repeated group Response = 1 { 1694 | optional ResponsePropertiesProto responseProperties = 2; 1695 | optional AssetsResponseProto assetsResponse = 3; 1696 | optional CommentsResponseProto commentsResponse = 4; 1697 | optional ModifyCommentResponseProto modifyCommentResponse = 5; 1698 | optional PurchasePostResponseProto purchasePostResponse = 6; 1699 | optional PurchaseOrderResponseProto purchaseOrderResponse = 7; 1700 | optional ContentSyncResponseProto contentSyncResponse = 8; 1701 | optional GetAssetResponseProto getAssetResponse = 9; 1702 | optional GetImageResponseProto getImageResponse = 10; 1703 | optional RefundResponseProto refundResponse = 11; 1704 | optional PurchaseMetadataResponseProto purchaseMetadataResponse = 12; 1705 | optional GetSubCategoriesResponseProto subCategoriesResponse = 13; 1706 | optional UninstallReasonResponseProto uninstallReasonResponse = 15; 1707 | optional RateCommentResponseProto rateCommentResponse = 16; 1708 | optional CheckLicenseResponseProto checkLicenseResponse = 17; 1709 | optional GetMarketMetadataResponseProto getMarketMetadataResponse = 18; 1710 | repeated PrefetchedBundleProto prefetchedBundle = 19; 1711 | optional GetCategoriesResponseProto getCategoriesResponse = 20; 1712 | optional GetCarrierInfoResponseProto getCarrierInfoResponse = 21; 1713 | optional RestoreApplicationsResponseProto restoreApplicationResponse = 23; 1714 | optional QuerySuggestionResponseProto querySuggestionResponse = 24; 1715 | optional BillingEventResponseProto billingEventResponse = 25; 1716 | optional PaypalPreapprovalResponseProto paypalPreapprovalResponse = 26; 1717 | optional PaypalPreapprovalDetailsResponseProto paypalPreapprovalDetailsResponse = 27; 1718 | optional PaypalCreateAccountResponseProto paypalCreateAccountResponse = 28; 1719 | optional PaypalPreapprovalCredentialsResponseProto paypalPreapprovalCredentialsResponse = 29; 1720 | optional InAppRestoreTransactionsResponseProto inAppRestoreTransactionsResponse = 30; 1721 | optional InAppPurchaseInformationResponseProto inAppPurchaseInformationResponse = 31; 1722 | optional CheckForNotificationsResponseProto checkForNotificationsResponse = 32; 1723 | optional AckNotificationsResponseProto ackNotificationsResponse = 33; 1724 | optional PurchaseProductResponseProto purchaseProductResponse = 34; 1725 | optional ReconstructDatabaseResponseProto reconstructDatabaseResponse = 35; 1726 | optional PaypalMassageAddressResponseProto paypalMassageAddressResponse = 36; 1727 | optional GetAddressSnippetResponseProto getAddressSnippetResponse = 37; 1728 | } 1729 | optional PendingNotificationsProto pendingNotifications = 38; 1730 | } 1731 | message RestoreApplicationsRequestProto { 1732 | optional string backupAndroidId = 1; 1733 | optional string tosVersion = 2; 1734 | optional DeviceConfigurationProto deviceConfiguration = 3; 1735 | } 1736 | message RestoreApplicationsResponseProto { 1737 | repeated GetAssetResponseProto asset = 1; 1738 | } 1739 | message RiskHeaderInfoProto { 1740 | optional string hashedDeviceInfo = 1; 1741 | } 1742 | message SignatureHashProto { 1743 | optional string packageName = 1; 1744 | optional int32 versionCode = 2; 1745 | optional bytes hash = 3; 1746 | } 1747 | message SignedDataProto { 1748 | optional string signedData = 1; 1749 | optional string signature = 2; 1750 | } 1751 | message SingleRequestProto { 1752 | optional RequestSpecificPropertiesProto requestSpecificProperties = 3; 1753 | optional AssetsRequestProto assetRequest = 4; 1754 | optional CommentsRequestProto commentsRequest = 5; 1755 | optional ModifyCommentRequestProto modifyCommentRequest = 6; 1756 | optional PurchasePostRequestProto purchasePostRequest = 7; 1757 | optional PurchaseOrderRequestProto purchaseOrderRequest = 8; 1758 | optional ContentSyncRequestProto contentSyncRequest = 9; 1759 | optional GetAssetRequestProto getAssetRequest = 10; 1760 | optional GetImageRequestProto getImageRequest = 11; 1761 | optional RefundRequestProto refundRequest = 12; 1762 | optional PurchaseMetadataRequestProto purchaseMetadataRequest = 13; 1763 | optional GetSubCategoriesRequestProto subCategoriesRequest = 14; 1764 | optional UninstallReasonRequestProto uninstallReasonRequest = 16; 1765 | optional RateCommentRequestProto rateCommentRequest = 17; 1766 | optional CheckLicenseRequestProto checkLicenseRequest = 18; 1767 | optional GetMarketMetadataRequestProto getMarketMetadataRequest = 19; 1768 | optional GetCategoriesRequestProto getCategoriesRequest = 21; 1769 | optional GetCarrierInfoRequestProto getCarrierInfoRequest = 22; 1770 | optional RemoveAssetRequestProto removeAssetRequest = 23; 1771 | optional RestoreApplicationsRequestProto restoreApplicationsRequest = 24; 1772 | optional QuerySuggestionRequestProto querySuggestionRequest = 25; 1773 | optional BillingEventRequestProto billingEventRequest = 26; 1774 | optional PaypalPreapprovalRequestProto paypalPreapprovalRequest = 27; 1775 | optional PaypalPreapprovalDetailsRequestProto paypalPreapprovalDetailsRequest = 28; 1776 | optional PaypalCreateAccountRequestProto paypalCreateAccountRequest = 29; 1777 | optional PaypalPreapprovalCredentialsRequestProto paypalPreapprovalCredentialsRequest = 30; 1778 | optional InAppRestoreTransactionsRequestProto inAppRestoreTransactionsRequest = 31; 1779 | optional InAppPurchaseInformationRequestProto getInAppPurchaseInformationRequest = 32; 1780 | optional CheckForNotificationsRequestProto checkForNotificationsRequest = 33; 1781 | optional AckNotificationsRequestProto ackNotificationsRequest = 34; 1782 | optional PurchaseProductRequestProto purchaseProductRequest = 35; 1783 | optional ReconstructDatabaseRequestProto reconstructDatabaseRequest = 36; 1784 | optional PaypalMassageAddressRequestProto paypalMassageAddressRequest = 37; 1785 | optional GetAddressSnippetRequestProto getAddressSnippetRequest = 38; 1786 | } 1787 | message SingleResponseProto { 1788 | optional ResponsePropertiesProto responseProperties = 2; 1789 | optional AssetsResponseProto assetsResponse = 3; 1790 | optional CommentsResponseProto commentsResponse = 4; 1791 | optional ModifyCommentResponseProto modifyCommentResponse = 5; 1792 | optional PurchasePostResponseProto purchasePostResponse = 6; 1793 | optional PurchaseOrderResponseProto purchaseOrderResponse = 7; 1794 | optional ContentSyncResponseProto contentSyncResponse = 8; 1795 | optional GetAssetResponseProto getAssetResponse = 9; 1796 | optional GetImageResponseProto getImageResponse = 10; 1797 | optional RefundResponseProto refundResponse = 11; 1798 | optional PurchaseMetadataResponseProto purchaseMetadataResponse = 12; 1799 | optional GetSubCategoriesResponseProto subCategoriesResponse = 13; 1800 | optional UninstallReasonResponseProto uninstallReasonResponse = 15; 1801 | optional RateCommentResponseProto rateCommentResponse = 16; 1802 | optional CheckLicenseResponseProto checkLicenseResponse = 17; 1803 | optional GetMarketMetadataResponseProto getMarketMetadataResponse = 18; 1804 | optional GetCategoriesResponseProto getCategoriesResponse = 20; 1805 | optional GetCarrierInfoResponseProto getCarrierInfoResponse = 21; 1806 | optional RestoreApplicationsResponseProto restoreApplicationResponse = 23; 1807 | optional QuerySuggestionResponseProto querySuggestionResponse = 24; 1808 | optional BillingEventResponseProto billingEventResponse = 25; 1809 | optional PaypalPreapprovalResponseProto paypalPreapprovalResponse = 26; 1810 | optional PaypalPreapprovalDetailsResponseProto paypalPreapprovalDetailsResponse = 27; 1811 | optional PaypalCreateAccountResponseProto paypalCreateAccountResponse = 28; 1812 | optional PaypalPreapprovalCredentialsResponseProto paypalPreapprovalCredentialsResponse = 29; 1813 | optional InAppRestoreTransactionsResponseProto inAppRestoreTransactionsResponse = 30; 1814 | optional InAppPurchaseInformationResponseProto getInAppPurchaseInformationResponse = 31; 1815 | optional CheckForNotificationsResponseProto checkForNotificationsResponse = 32; 1816 | optional AckNotificationsResponseProto ackNotificationsResponse = 33; 1817 | optional PurchaseProductResponseProto purchaseProductResponse = 34; 1818 | optional ReconstructDatabaseResponseProto reconstructDatabaseResponse = 35; 1819 | optional PaypalMassageAddressResponseProto paypalMassageAddressResponse = 36; 1820 | optional GetAddressSnippetResponseProto getAddressSnippetResponse = 37; 1821 | } 1822 | message StatusBarNotificationProto { 1823 | optional string tickerText = 1; 1824 | optional string contentTitle = 2; 1825 | optional string contentText = 3; 1826 | } 1827 | message UninstallReasonRequestProto { 1828 | optional string assetId = 1; 1829 | optional int32 reason = 2; 1830 | } 1831 | message UninstallReasonResponseProto { 1832 | } 1833 | -------------------------------------------------------------------------------- /googleplay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import base64 4 | import gzip 5 | import pprint 6 | import StringIO 7 | import requests 8 | 9 | from google.protobuf import descriptor 10 | from google.protobuf.internal.containers import RepeatedCompositeFieldContainer 11 | from google.protobuf import text_format 12 | from google.protobuf.message import Message, DecodeError 13 | 14 | import googleplay_pb2 15 | import config 16 | 17 | import sys 18 | import time 19 | 20 | class LoginError(Exception): 21 | def __init__(self, value): 22 | self.value = value 23 | def __str__(self): 24 | return repr(self.value) 25 | 26 | class RequestError(Exception): 27 | def __init__(self, value): 28 | self.value = value 29 | def __str__(self): 30 | return repr(self.value) 31 | 32 | class GooglePlayAPI(object): 33 | """Google Play Unofficial API Class 34 | 35 | Usual APIs methods are login(), search(), details(), bulkDetails(), 36 | download(), browse(), reviews() and list(). 37 | 38 | toStr() can be used to pretty print the result (protobuf object) of the 39 | previous methods. 40 | 41 | toDict() converts the result into a dict, for easier introspection.""" 42 | 43 | SERVICE = "androidmarket" 44 | URL_LOGIN = "https://android.clients.google.com/auth" # "https://www.google.com/accounts/ClientLogin" 45 | ACCOUNT_TYPE_GOOGLE = "GOOGLE" 46 | ACCOUNT_TYPE_HOSTED = "HOSTED" 47 | ACCOUNT_TYPE_HOSTED_OR_GOOGLE = "HOSTED_OR_GOOGLE" 48 | authSubToken = None 49 | 50 | def __init__(self, androidId=None, lang=None, debug=False): # you must use a device-associated androidId value 51 | self.preFetch = {} 52 | if androidId == None: 53 | androidId = config.ANDROID_ID 54 | if lang == None: 55 | lang = config.LANG 56 | self.androidId = androidId 57 | self.lang = lang 58 | self.debug = debug 59 | 60 | def toDict(self, protoObj): 61 | """Converts the (protobuf) result from an API call into a dict, for 62 | easier introspection.""" 63 | iterable = False 64 | if isinstance(protoObj, RepeatedCompositeFieldContainer): 65 | iterable = True 66 | else: 67 | protoObj = [protoObj] 68 | retlist = [] 69 | 70 | for po in protoObj: 71 | msg = dict() 72 | for fielddesc, value in po.ListFields(): 73 | #print value, type(value), getattr(value, "__iter__", False) 74 | if fielddesc.type == descriptor.FieldDescriptor.TYPE_GROUP or isinstance(value, RepeatedCompositeFieldContainer) or isinstance(value, Message): 75 | msg[fielddesc.name] = self.toDict(value) 76 | else: 77 | msg[fielddesc.name] = value 78 | retlist.append(msg) 79 | if not iterable: 80 | if len(retlist) > 0: 81 | return retlist[0] 82 | else: 83 | return None 84 | return retlist 85 | 86 | def toStr(self, protoObj): 87 | """Used for pretty printing a result from the API.""" 88 | return text_format.MessageToString(protoObj) 89 | 90 | def _try_register_preFetch(self, protoObj): 91 | fields = [i.name for (i,_) in protoObj.ListFields()] 92 | if ("preFetch" in fields): 93 | for p in protoObj.preFetch: 94 | self.preFetch[p.url] = p.response 95 | 96 | def setAuthSubToken(self, authSubToken): 97 | self.authSubToken = authSubToken 98 | 99 | # put your auth token in config.py to avoid multiple login requests 100 | if self.debug: 101 | print "authSubToken: " + authSubToken 102 | 103 | def login(self, email=None, password=None, authSubToken=None): 104 | """Login to your Google Account. You must provide either: 105 | - an email and password 106 | - a valid Google authSubToken""" 107 | if (authSubToken is not None): 108 | self.setAuthSubToken(authSubToken) 109 | else: 110 | if (email is None or password is None): 111 | raise Exception("You should provide at least authSubToken or (email and password)") 112 | params = {"Email": email, 113 | "Passwd": password, 114 | "service": self.SERVICE, 115 | "accountType": self.ACCOUNT_TYPE_HOSTED_OR_GOOGLE, 116 | "has_permission": "1", 117 | "source": "android", 118 | "androidId": self.androidId, 119 | "app": "com.android.vending", 120 | #"client_sig": self.client_sig, 121 | "device_country": "us", 122 | "operatorCountry": "us", 123 | "lang": "us", 124 | "sdk_version": "23"} 125 | headers = { 126 | "Accept-Encoding": "", 127 | } 128 | response = requests.post(self.URL_LOGIN, data=params, headers=headers, verify=False) 129 | data = response.text.split() 130 | params = {} 131 | for d in data: 132 | if not "=" in d: continue 133 | try: 134 | k, v = d.split("=") 135 | except Exception as e: 136 | print >> sys.stderr, "googleplay.py", d 137 | raise e 138 | params[k.strip().lower()] = v.strip() 139 | if "auth" in params: 140 | self.setAuthSubToken(params["auth"]) 141 | elif "error" in params: 142 | raise LoginError("server says: " + params["error"]) 143 | else: 144 | raise LoginError("Auth token not found.") 145 | 146 | def executeRequestApi2(self, path, datapost=None, post_content_type="application/x-www-form-urlencoded; charset=UTF-8"): 147 | if (datapost is None and path in self.preFetch): 148 | data = self.preFetch[path] 149 | else: 150 | headers = { "Accept-Language": self.lang, 151 | "Authorization": "GoogleLogin auth=%s" % self.authSubToken, 152 | "X-DFE-Enabled-Experiments": "cl:billing.select_add_instrument_by_default", 153 | "X-DFE-Unsupported-Experiments": "nocache:billing.use_charging_poller,market_emails,buyer_currency,prod_baseline,checkin.set_asset_paid_app_field,shekel_test,content_ratings,buyer_currency_in_app,nocache:encrypted_apk,recent_changes", 154 | "X-DFE-Device-Id": self.androidId, 155 | "X-DFE-Client-Id": "am-android-google", 156 | #"X-DFE-Logging-Id": self.loggingId2, # Deprecated? 157 | #"User-Agent": "Android-Finsky/3.7.13 (api=3,versionCode=8013013,sdk=16,device=crespo,hardware=herring,product=soju)", 158 | "User-Agent": "Android-Finsky/6.7.24 (api=3,versionCode=80682400,sdk=23,device=hammerhead,hardware=hammerhead,product=hammerhead)", 159 | "X-DFE-SmallestScreenWidthDp": "320", 160 | "X-DFE-Filter-Level": "3", 161 | "Accept-Encoding": "", 162 | "Host": "android.clients.google.com"} 163 | 164 | if datapost is not None: 165 | headers["Content-Type"] = post_content_type 166 | 167 | url = "https://android.clients.google.com/fdfe/%s" % path 168 | if datapost is not None: 169 | response = requests.post(url, data=datapost, headers=headers, verify=False) 170 | else: 171 | response = requests.get(url, headers=headers, verify=False) 172 | data = response.content 173 | 174 | ''' 175 | data = StringIO.StringIO(data) 176 | gzipper = gzip.GzipFile(fileobj=data) 177 | data = gzipper.read() 178 | ''' 179 | 180 | # Sometimes the protobuf response might have an 181 | # "Unexpected end-group tag." error. Retrying usually resolves it. 182 | numRetry = 5 183 | for i in xrange(0, numRetry): 184 | try: 185 | message = googleplay_pb2.ResponseWrapper.FromString(data) 186 | except DecodeError as e: 187 | if i == (numRetry - 1): 188 | raise e 189 | else: 190 | time.sleep(3) 191 | continue 192 | break 193 | 194 | self._try_register_preFetch(message) 195 | 196 | # Debug 197 | #print text_format.MessageToString(message) 198 | return message 199 | 200 | ##################################### 201 | # Google Play API Methods 202 | ##################################### 203 | 204 | def search(self, query, nb_results=None, offset=None): 205 | """Search for apps.""" 206 | path = "search?c=3&q=%s" % requests.utils.quote(query) # TODO handle categories 207 | if (nb_results is not None): 208 | path += "&n=%d" % int(nb_results) 209 | if (offset is not None): 210 | path += "&o=%d" % int(offset) 211 | 212 | message = self.executeRequestApi2(path) 213 | return message.payload.searchResponse 214 | 215 | def details(self, packageName): 216 | """Get app details from a package name. 217 | packageName is the app unique ID (usually starting with 'com.').""" 218 | path = "details?doc=%s" % requests.utils.quote(packageName) 219 | message = self.executeRequestApi2(path) 220 | return message.payload.detailsResponse 221 | 222 | def bulkDetails(self, packageNames): 223 | """Get several apps details from a list of package names. 224 | 225 | This is much more efficient than calling N times details() since it 226 | requires only one request. 227 | 228 | packageNames is a list of app ID (usually starting with 'com.').""" 229 | path = "bulkDetails" 230 | req = googleplay_pb2.BulkDetailsRequest() 231 | req.docid.extend(packageNames) 232 | data = req.SerializeToString() 233 | message = self.executeRequestApi2(path, data, "application/x-protobuf") 234 | return message.payload.bulkDetailsResponse 235 | 236 | def browse(self, cat=None, ctr=None): 237 | """Browse categories. 238 | cat (category ID) and ctr (subcategory ID) are used as filters.""" 239 | path = "browse?c=3" 240 | if (cat != None): 241 | path += "&cat=%s" % requests.utils.quote(cat) 242 | if (ctr != None): 243 | path += "&ctr=%s" % requests.utils.quote(ctr) 244 | message = self.executeRequestApi2(path) 245 | return message.payload.browseResponse 246 | 247 | def list(self, cat, ctr=None, nb_results=None, offset=None): 248 | """List apps. 249 | 250 | If ctr (subcategory ID) is None, returns a list of valid subcategories. 251 | 252 | If ctr is provided, list apps within this subcategory.""" 253 | path = "list?c=3&cat=%s" % requests.utils.quote(cat) 254 | if (ctr != None): 255 | path += "&ctr=%s" % requests.utils.quote(ctr) 256 | if (nb_results != None): 257 | path += "&n=%s" % requests.utils.quote(nb_results) 258 | if (offset != None): 259 | path += "&o=%s" % requests.utils.quote(offset) 260 | message = self.executeRequestApi2(path) 261 | return message.payload.listResponse 262 | 263 | def reviews(self, packageName, filterByDevice=False, sort=2, nb_results=None, offset=None): 264 | """Browse reviews. 265 | packageName is the app unique ID. 266 | If filterByDevice is True, return only reviews for your device.""" 267 | path = "rev?doc=%s&sort=%d" % (requests.utils.quote(packageName), sort) 268 | if (nb_results is not None): 269 | path += "&n=%d" % int(nb_results) 270 | if (offset is not None): 271 | path += "&o=%d" % int(offset) 272 | if(filterByDevice): 273 | path += "&dfil=1" 274 | message = self.executeRequestApi2(path) 275 | return message.payload.reviewResponse 276 | 277 | def download(self, packageName, versionCode, offerType=1): 278 | """Download an app and return its raw data (APK file). 279 | 280 | packageName is the app unique ID (usually starting with 'com.'). 281 | 282 | versionCode can be grabbed by using the details() method on the given 283 | app.""" 284 | path = "purchase" 285 | data = "ot=%d&doc=%s&vc=%d" % (offerType, packageName, versionCode) 286 | message = self.executeRequestApi2(path, data) 287 | 288 | url = message.payload.buyResponse.purchaseStatusResponse.appDeliveryData.downloadUrl 289 | 290 | # There was an error 291 | if len(message.commands.displayErrorMessage) is not 0: 292 | raise Exception(message.commands.displayErrorMessage) 293 | 294 | try: 295 | cookie = message.payload.buyResponse.purchaseStatusResponse.appDeliveryData.downloadAuthCookie[0] 296 | except Exception as e: 297 | print >> sys.stderr, "googleplay.py", message.payload.buyResponse.purchaseStatusResponse.appDeliveryData.downloadAuthCookie 298 | raise e 299 | 300 | cookies = { 301 | str(cookie.name): str(cookie.value) # python-requests #459 fixes this 302 | } 303 | 304 | headers = { 305 | "User-Agent" : "AndroidDownloadManager/6.0.1 (Linux; U; Android 6.0.1; Nexus 5 Build/MOB30P)", 306 | #"User-Agent" : "AndroidDownloadManager/4.1.1 (Linux; U; Android 4.1.1; Nexus S Build/JRO03E)", 307 | "Accept-Encoding": "", 308 | } 309 | 310 | response = requests.get(url, headers=headers, cookies=cookies, verify=False) 311 | return response.content 312 | 313 | -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | from config import SEPARATOR 2 | 3 | def sizeof_fmt(num): 4 | for x in ['bytes','KB','MB','GB','TB']: 5 | if num < 1024.0: 6 | return "%3.1f%s" % (num, x) 7 | num /= 1024.0 8 | 9 | def print_header_line(): 10 | l = [ "Title", 11 | "Package name", 12 | "Creator", 13 | "Super Dev", 14 | "Price", 15 | "Offer Type", 16 | "Version Code", 17 | "Size", 18 | "Rating", 19 | "Num Downloads", 20 | ] 21 | print SEPARATOR.join(l) 22 | 23 | def print_result_line(c): 24 | #c.offer[0].micros/1000000.0 25 | #c.offer[0].currencyCode 26 | l = [ c.title, 27 | c.docid, 28 | c.creator, 29 | len(c.annotations.badgeForCreator), # Is Super Developer? 30 | c.offer[0].formattedAmount, 31 | c.offer[0].offerType, 32 | c.details.appDetails.versionCode, 33 | sizeof_fmt(c.details.appDetails.installationSize), 34 | "%.2f" % c.aggregateRating.starRating, 35 | c.details.appDetails.numDownloads] 36 | print SEPARATOR.join(unicode(i).encode('utf8') for i in l) 37 | 38 | -------------------------------------------------------------------------------- /list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Do not remove 4 | GOOGLE_LOGIN = GOOGLE_PASSWORD = AUTH_TOKEN = None 5 | 6 | import sys 7 | from pprint import pprint 8 | 9 | from config import * 10 | from googleplay import GooglePlayAPI 11 | from helpers import sizeof_fmt, print_header_line, print_result_line 12 | 13 | if (len(sys.argv) < 2): 14 | print "Usage: %s category [subcategory] [nb_results] [offset]" % sys.argv[0] 15 | print "List subcategories and apps within them." 16 | print "category: To obtain a list of supported catagories, use categories.py" 17 | print "subcategory: You can get a list of all subcategories available, by supplying a valid category" 18 | sys.exit(0) 19 | 20 | cat = sys.argv[1] 21 | ctr = None 22 | nb_results = None 23 | offset = None 24 | 25 | if (len(sys.argv) >= 3): 26 | ctr = sys.argv[2] 27 | if (len(sys.argv) >= 4): 28 | nb_results = sys.argv[3] 29 | if (len(sys.argv) == 5): 30 | offset = sys.argv[4] 31 | 32 | api = GooglePlayAPI(ANDROID_ID) 33 | api.login(GOOGLE_LOGIN, GOOGLE_PASSWORD, AUTH_TOKEN) 34 | try: 35 | message = api.list(cat, ctr, nb_results, offset) 36 | except: 37 | print "Error: HTTP 500 - one of the provided parameters is invalid" 38 | 39 | if (ctr is None): 40 | print SEPARATOR.join(["Subcategory ID", "Name"]) 41 | for doc in message.doc: 42 | print SEPARATOR.join([doc.docid.encode('utf8'), doc.title.encode('utf8')]) 43 | else: 44 | print_header_line() 45 | doc = message.doc[0] 46 | for c in doc.child: 47 | print_result_line(c) 48 | 49 | -------------------------------------------------------------------------------- /permissions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Do not remove 4 | GOOGLE_LOGIN = GOOGLE_PASSWORD = AUTH_TOKEN = None 5 | 6 | import sys 7 | import urlparse 8 | from pprint import pprint 9 | from google.protobuf import text_format 10 | 11 | from config import * 12 | from googleplay import GooglePlayAPI 13 | 14 | if (len(sys.argv) < 2): 15 | print "Usage: %s packagename1 [packagename2 [...]]" % sys.argv[0] 16 | print "Display permissions required to install the specified app(s)." 17 | sys.exit(0) 18 | 19 | packagenames = sys.argv[1:] 20 | 21 | api = GooglePlayAPI(ANDROID_ID) 22 | api.login(GOOGLE_LOGIN, GOOGLE_PASSWORD, AUTH_TOKEN) 23 | 24 | # Only one app 25 | if (len(packagenames) == 1): 26 | response = api.details(packagenames[0]) 27 | print "\n".join(i.encode('utf8') for i in response.docV2.details.appDetails.permission) 28 | 29 | else: # More than one app 30 | response = api.bulkDetails(packagenames) 31 | 32 | for entry in response.entry: 33 | if (not not entry.ListFields()): # if the entry is not empty 34 | print entry.doc.docid + ":" 35 | print "\n".join(" "+i.encode('utf8') for i in entry.doc.details.appDetails.permission) 36 | print 37 | 38 | -------------------------------------------------------------------------------- /search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Do not remove 4 | GOOGLE_LOGIN = GOOGLE_PASSWORD = AUTH_TOKEN = None 5 | 6 | import sys 7 | from pprint import pprint 8 | 9 | from config import * 10 | from googleplay import GooglePlayAPI 11 | from helpers import sizeof_fmt, print_header_line, print_result_line 12 | 13 | if (len(sys.argv) < 2): 14 | print "Usage: %s request [nb_results] [offset]" % sys.argv[0] 15 | print "Search for an app." 16 | print "If request contains a space, don't forget to surround it with \"\"" 17 | sys.exit(0) 18 | 19 | request = sys.argv[1] 20 | nb_res = None 21 | offset = None 22 | 23 | if (len(sys.argv) >= 3): 24 | nb_res = int(sys.argv[2]) 25 | 26 | if (len(sys.argv) >= 4): 27 | offset = int(sys.argv[3]) 28 | 29 | api = GooglePlayAPI(ANDROID_ID) 30 | api.login(GOOGLE_LOGIN, GOOGLE_PASSWORD, AUTH_TOKEN) 31 | 32 | try: 33 | message = api.search(request, nb_res, offset) 34 | except: 35 | print "Error: something went wrong. Maybe the nb_res you specified was too big?" 36 | sys.exit(1) 37 | 38 | print_header_line() 39 | doc = message.doc[0] 40 | for c in doc.child: 41 | print_result_line(c) 42 | 43 | --------------------------------------------------------------------------------