ZAPKDownloader 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/ZAPKDownloader +91 -0
- data/lib/GoogleAPI.rb +103 -0
- metadata +74 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: b1b3fdd672542121466f767a3a574cac3c5806368e78d088ed72ab9b4ae20b5c
         | 
| 4 | 
            +
              data.tar.gz: df88d1b46bb54ff9fed0dbeb1e0fb00edab59c135d2903876107e8b0561671fd
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: f613f1a1c806d89942b696cdb41a90c63cce71197d5af5656618a71459aa148647019ab3c3c5166e66cf5d77fdf265d88da2cd29a005d65a83866a33e988e5e0
         | 
| 7 | 
            +
              data.tar.gz: 430d21c12bba8db930041888b9dfded34dba2d86cfb24f75d1bff86af089d3d5b4496bfb71d388ebafebc510214cc06f2078a0638355ea3536c7c4bd70ab488a
         | 
    
        data/bin/ZAPKDownloader
    ADDED
    
    | @@ -0,0 +1,91 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            # -*- coding: utf-8 -*-
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            $lib = File.expand_path('../lib', File.dirname(__FILE__))
         | 
| 5 | 
            +
            $LOAD_PATH.unshift($lib)
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require "GoogleAPI"
         | 
| 8 | 
            +
            require "optparse"
         | 
| 9 | 
            +
            require "yaml"
         | 
| 10 | 
            +
            require "fileutils"
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            class Main
         | 
| 13 | 
            +
                
         | 
| 14 | 
            +
                attr_accessor :googleAPI
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def initialize
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    ARGV << '-h' if ARGV.empty?
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    inputServiceAccount = nil
         | 
| 21 | 
            +
                    inputOutputFilePath = "."
         | 
| 22 | 
            +
                    inputOutputFileName = "Universal.apk"
         | 
| 23 | 
            +
                    inputPackageName = nil
         | 
| 24 | 
            +
                    inputVersionCode = nil
         | 
| 25 | 
            +
                    OptionParser.new do |opts|
         | 
| 26 | 
            +
                        opts.banner = "Usage: ZAPKDownloader [options]"
         | 
| 27 | 
            +
                        opts.on('-s', '--serviceAccount SERVICEACCOUNT', 'xxxx@xxx.iam.gserviceaccount.com.json') do |serviceAccount|
         | 
| 28 | 
            +
                            inputServiceAccount = serviceAccount
         | 
| 29 | 
            +
                        end 
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                        opts.on('-p', '--path FILEPATH', '/Users/zhgchgli/Downloads') do |filePath|
         | 
| 32 | 
            +
                            inputOutputFilePath = filePath
         | 
| 33 | 
            +
                        end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                        opts.on('-f', '--fileName FILENAME', '/Users/zhgchgli/Downloads') do |fileName|
         | 
| 36 | 
            +
                            inputOutputFileName = fileName
         | 
| 37 | 
            +
                        end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                        opts.on('-n', '--packageName PACKAGENAME', 'com.zhgchg') do |packageName|
         | 
| 40 | 
            +
                            inputPackageName = packageName
         | 
| 41 | 
            +
                        end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                        opts.on('-v', '--versionCode VERSIONCODE', '1203') do |versionCode|
         | 
| 44 | 
            +
                            inputVersionCode = versionCode
         | 
| 45 | 
            +
                        end
         | 
| 46 | 
            +
                    end.parse!
         | 
| 47 | 
            +
                    
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    if inputServiceAccount.nil? 
         | 
| 50 | 
            +
                        puts "please specify your service account json auth file. e.g. -f xxxx@xxx.iam.gserviceaccount.com.json"
         | 
| 51 | 
            +
                        return
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    if inputPackageName.nil? 
         | 
| 55 | 
            +
                        puts "please specify your android app package name. e.g. -n com.zhgchg"
         | 
| 56 | 
            +
                        return
         | 
| 57 | 
            +
                    end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    if inputVersionCode.nil? 
         | 
| 60 | 
            +
                        puts "please specify your android app's version code that you want to download. e.g. -v 1203"
         | 
| 61 | 
            +
                        return
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    @googleAPI = GoogleAPI.new(inputServiceAccount, ["https://www.googleapis.com/auth/androidpublisher"])
         | 
| 65 | 
            +
                    apks = googleAPI.request("https://androidpublisher.googleapis.com/androidpublisher/v3/applications/#{inputPackageName}/generatedApks/#{inputVersionCode}")
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    downloadId = apks["generatedApks"][0]["generatedUniversalApk"]["downloadId"]
         | 
| 68 | 
            +
                    downloadURL = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/#{inputPackageName}/generatedApks/1327/downloads/#{downloadId}:download?alt=media"
         | 
| 69 | 
            +
                    
         | 
| 70 | 
            +
                    puts "PackageName:#{inputPackageName}, versionCode:#{inputVersionCode}"
         | 
| 71 | 
            +
                    puts "#{inputOutputFileName} Downloading..."
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    uri = URI(downloadURL)
         | 
| 74 | 
            +
                    https = Net::HTTP.new(uri.host, uri.port)
         | 
| 75 | 
            +
                    https.use_ssl = true
         | 
| 76 | 
            +
                    
         | 
| 77 | 
            +
                    request = Net::HTTP::Get.new(uri)
         | 
| 78 | 
            +
                    request['Authorization'] = "Bearer #{googleAPI.token}";
         | 
| 79 | 
            +
                    
         | 
| 80 | 
            +
                    response = https.request(request).read_body
         | 
| 81 | 
            +
                    
         | 
| 82 | 
            +
                    fileFullPath = "#{inputOutputFilePath}/#{inputOutputFileName}"
         | 
| 83 | 
            +
                    File.delete(fileFullPath) if File.exist?(fileFullPath)
         | 
| 84 | 
            +
                    
         | 
| 85 | 
            +
                    open(fileFullPath, "wb") { |file|
         | 
| 86 | 
            +
                        file.write(response)
         | 
| 87 | 
            +
                    }
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
            end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
            Main.new()
         | 
    
        data/lib/GoogleAPI.rb
    ADDED
    
    | @@ -0,0 +1,103 @@ | |
| 1 | 
            +
            $lib = File.expand_path('../lib', File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "jwt"
         | 
| 4 | 
            +
            require "time"
         | 
| 5 | 
            +
            require "net/http"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            class GoogleAPI
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                attr_accessor :keyContent, :keyID, :tokenURI, :clientEmail, :scopes, :token
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(keyFilePath, scopes = [])
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    keyFileContent = JSON.parse(File.read(keyFilePath))
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    @keyContent = keyFileContent["private_key"]
         | 
| 16 | 
            +
                    @keyID = keyFileContent["private_key_id"]
         | 
| 17 | 
            +
                    @clientEmail = keyFileContent["client_email"]
         | 
| 18 | 
            +
                    @tokenURI = keyFileContent["token_uri"]
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    @scopes = scopes
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    @token = generateJWT()
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    puts "[GoogleAPI] Init Success."
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def request(url, method = "GET", data = nil, retryCount = 0)
         | 
| 28 | 
            +
                    uri = URI(url)
         | 
| 29 | 
            +
                    https = Net::HTTP.new(uri.host, uri.port)
         | 
| 30 | 
            +
                    https.use_ssl = true
         | 
| 31 | 
            +
                    
         | 
| 32 | 
            +
                    request = Net::HTTP::Get.new(uri)
         | 
| 33 | 
            +
                    if method.upcase == "POST"
         | 
| 34 | 
            +
                        request = Net::HTTP::Post.new(uri)
         | 
| 35 | 
            +
                        if !data.nil?
         | 
| 36 | 
            +
                            request['Content-Type'] = 'application/json'
         | 
| 37 | 
            +
                            request.body = JSON.dump(data)
         | 
| 38 | 
            +
                        end
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
                    
         | 
| 41 | 
            +
                    request['Authorization'] = "Bearer #{token}";
         | 
| 42 | 
            +
                    
         | 
| 43 | 
            +
                    response = https.request(request).read_body
         | 
| 44 | 
            +
                    result = JSON.parse(response)
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    if !result["error"].nil?
         | 
| 47 | 
            +
                        # Quota exceeded for quota metric 'Write requests' and limit 'Write requests per minute per user
         | 
| 48 | 
            +
                        if result["error"]["code"] == 429
         | 
| 49 | 
            +
                            puts "[GoogleAPI] Reached Rate Limited, sleep 30 secs..."
         | 
| 50 | 
            +
                            sleep(30)
         | 
| 51 | 
            +
                            return request(url, method, data, 0)
         | 
| 52 | 
            +
                        else
         | 
| 53 | 
            +
                            if retryCount >= 10
         | 
| 54 | 
            +
                                raise "Could not connect to #{tokenURI}, key id: #{keyID}, error message: #{response}"
         | 
| 55 | 
            +
                            else
         | 
| 56 | 
            +
                                @token = generateJWT()
         | 
| 57 | 
            +
                                message = "JWT Invalid, retry. (#{retryCount + 1})"
         | 
| 58 | 
            +
                                puts "[GoogleAPI] #{message}"
         | 
| 59 | 
            +
                                return request(url, method, data, retryCount + 1)
         | 
| 60 | 
            +
                            end
         | 
| 61 | 
            +
                        end
         | 
| 62 | 
            +
                    else
         | 
| 63 | 
            +
                        return result
         | 
| 64 | 
            +
                    end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                private
         | 
| 69 | 
            +
                def generateJWT(retryCount = 0)
         | 
| 70 | 
            +
                    payload = {
         | 
| 71 | 
            +
                        iss: clientEmail,
         | 
| 72 | 
            +
                        sub: clientEmail,
         | 
| 73 | 
            +
                        scope: scopes.join(' '),
         | 
| 74 | 
            +
                        aud: tokenURI,
         | 
| 75 | 
            +
                        iat: Time.now.to_i,
         | 
| 76 | 
            +
                        exp: Time.now.to_i + 60*20
         | 
| 77 | 
            +
                    }
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                    rsa_private = OpenSSL::PKey::RSA.new(keyContent)
         | 
| 80 | 
            +
                    token = JWT.encode payload, rsa_private, 'RS256', header_fields = {kid:keyID, typ:"JWT"}
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                    uri = URI(tokenURI)
         | 
| 83 | 
            +
                    https = Net::HTTP.new(uri.host, uri.port)
         | 
| 84 | 
            +
                    https.use_ssl = true
         | 
| 85 | 
            +
                    request = Net::HTTP::Post.new(uri)
         | 
| 86 | 
            +
                    request.body = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=#{token}"
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                    response = https.request(request).read_body
         | 
| 89 | 
            +
                    result = JSON.parse(response)
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    if result["access_token"].nil?
         | 
| 92 | 
            +
                        if retryCount >= 10
         | 
| 93 | 
            +
                            raise "Could not generate google api JWT, key id: #{keyID}, error message: #{response}"
         | 
| 94 | 
            +
                        else
         | 
| 95 | 
            +
                            message = "Could not generate google api JWT, retry. (#{retryCount + 1})"
         | 
| 96 | 
            +
                            puts "[GoogleAPI] #{message}"
         | 
| 97 | 
            +
                            return generateJWT(retryCount + 1)
         | 
| 98 | 
            +
                        end
         | 
| 99 | 
            +
                    else
         | 
| 100 | 
            +
                        return result["access_token"]
         | 
| 101 | 
            +
                    end
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,74 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: ZAPKDownloader
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 1.0.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - ZhgChgLi
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: bin
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2023-04-14 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: net-http
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - "~>"
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: 0.1.0
         | 
| 20 | 
            +
              type: :runtime
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - "~>"
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: 0.1.0
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: jwt
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - "~>"
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: 2.4.1
         | 
| 34 | 
            +
              type: :runtime
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - "~>"
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: 2.4.1
         | 
| 41 | 
            +
            description: ZAPKDownloader
         | 
| 42 | 
            +
            email: 
         | 
| 43 | 
            +
            executables:
         | 
| 44 | 
            +
            - ZAPKDownloader
         | 
| 45 | 
            +
            extensions: []
         | 
| 46 | 
            +
            extra_rdoc_files: []
         | 
| 47 | 
            +
            files:
         | 
| 48 | 
            +
            - bin/ZAPKDownloader
         | 
| 49 | 
            +
            - lib/GoogleAPI.rb
         | 
| 50 | 
            +
            homepage: https://github.com/ZhgChgLi/ZAPKDownloader
         | 
| 51 | 
            +
            licenses:
         | 
| 52 | 
            +
            - MIT
         | 
| 53 | 
            +
            metadata: {}
         | 
| 54 | 
            +
            post_install_message: 
         | 
| 55 | 
            +
            rdoc_options: []
         | 
| 56 | 
            +
            require_paths:
         | 
| 57 | 
            +
            - lib
         | 
| 58 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 59 | 
            +
              requirements:
         | 
| 60 | 
            +
              - - ">="
         | 
| 61 | 
            +
                - !ruby/object:Gem::Version
         | 
| 62 | 
            +
                  version: '0'
         | 
| 63 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 64 | 
            +
              requirements:
         | 
| 65 | 
            +
              - - ">="
         | 
| 66 | 
            +
                - !ruby/object:Gem::Version
         | 
| 67 | 
            +
                  version: '0'
         | 
| 68 | 
            +
            requirements: []
         | 
| 69 | 
            +
            rubygems_version: 3.0.3
         | 
| 70 | 
            +
            signing_key: 
         | 
| 71 | 
            +
            specification_version: 4
         | 
| 72 | 
            +
            summary: A tool that helps developers to download their uploaded APK files from the
         | 
| 73 | 
            +
              Google Play Console via CLI.
         | 
| 74 | 
            +
            test_files: []
         |