ETLane 0.1.42 → 0.1.46

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,165 @@
1
+ require 'base64'
2
+
3
+ module Fastlane
4
+ module Actions
5
+ class LokaliseUploadAction < Action
6
+ def self.run(params)
7
+
8
+ self.snapshot(params)
9
+ sources = params[:sources]
10
+ languages = params[:languages]
11
+ accepted_formats = [".strings", ".stringsdict"]
12
+ for source in sources do
13
+ Dir.chdir(source) do
14
+ Dir.glob('*').select { |f| File.directory? f
15
+ for lang in languages
16
+ folder = "#{lang}.lproj"
17
+ if f == folder
18
+ Dir.chdir(folder) do
19
+ Dir.glob('*').select { |f| accepted_formats.include? File.extname(f)
20
+ upload_name = File.join(source, folder, f)
21
+ File.open(f, 'rb') do |img|
22
+ base64 = Base64.strict_encode64(img.read)
23
+ upload(lang, upload_name, base64, params)
24
+ end
25
+ }
26
+ end
27
+ end
28
+ end
29
+
30
+ }
31
+ end
32
+ end
33
+
34
+
35
+ # UI.error "Bad response 🉐\n#{response.body}" unless jsonResponse.kind_of? Hash
36
+ # fileURL = jsonResponse["bundle_url"]
37
+ # if fileURL.kind_of?(String) then
38
+ # UI.message "Downloading localizations archive 📦"
39
+ # FileUtils.mkdir_p("lokalisetmp")
40
+ # uri = URI(fileURL)
41
+ # http = Net::HTTP.new(uri.host, uri.port)
42
+ # http.use_ssl = true
43
+ # zipRequest = Net::HTTP::Get.new(uri)
44
+ # response = http.request(zipRequest)
45
+ # if response.content_type == "application/zip" or response.content_type == "application/octet-stream" then
46
+ # FileUtils.mkdir_p("lokalisetmp")
47
+ # open("lokalisetmp/a.zip", "wb") { |file|
48
+ # file.write(response.body)
49
+ # }
50
+ # unzip_file("lokalisetmp/a.zip", destination, clean_destination)
51
+ # FileUtils.remove_dir("lokalisetmp")
52
+ # UI.success "Localizations extracted to #{destination} 📗 📕 📘"
53
+ # else
54
+ # UI.error "Response did not include ZIP"
55
+ # end
56
+ # elsif jsonResponse["response"]["status"] == "error"
57
+ # code = jsonResponse["response"]["code"]
58
+ # message = jsonResponse["response"]["message"]
59
+ # UI.error "Response error code #{code} (#{message}) 📟"
60
+ # else
61
+ # UI.error "Bad response 🉐\n#{jsonResponse}"
62
+ # end
63
+ end
64
+
65
+ def self.snapshot(params)
66
+ require 'net/http'
67
+ project_identifier = params[:project_identifier]
68
+ uri = URI("https://api.lokalise.com/api2/projects/#{project_identifier}/snapshots")
69
+ request_data = {
70
+ title: params[:snapshot_version],
71
+ }
72
+ request = Net::HTTP::Post.new(uri)
73
+ request.body = request_data.to_json
74
+ request['X-Api-Token'] = params[:api_token]
75
+ request['Content-Type'] = 'application/json'
76
+ http = Net::HTTP.new(uri.host, uri.port)
77
+ http.use_ssl = true
78
+ response = http.request(request)
79
+ jsonResponse = JSON.parse(response.body)
80
+ puts jsonResponse
81
+ end
82
+
83
+ def self.upload(lang, name, data, params)
84
+ require 'net/http'
85
+ token = params[:api_token]
86
+ project_identifier = params[:project_identifier]
87
+ if lang == "es-419"
88
+ lang = "es_419"
89
+ end
90
+ puts "Lang: #{lang} #{name}"
91
+ request_data = {
92
+ filename: name,
93
+ data: data,
94
+ lang_iso: lang,
95
+ cleanup_mode: true,
96
+ }
97
+ uri = URI("https://api.lokalise.com/api2/projects/#{project_identifier}/files/upload")
98
+ request = Net::HTTP::Post.new(uri)
99
+ request.body = request_data.to_json
100
+ request['X-Api-Token'] = token
101
+ request['Content-Type'] = 'application/json'
102
+ http = Net::HTTP.new(uri.host, uri.port)
103
+ http.use_ssl = true
104
+ response = http.request(request)
105
+ jsonResponse = JSON.parse(response.body)
106
+ puts jsonResponse
107
+
108
+ end
109
+
110
+ #####################################################
111
+ # @!group Documentation
112
+ #####################################################
113
+
114
+ def self.description
115
+ "Upload Lokalise localization"
116
+ end
117
+
118
+ def self.available_options
119
+ [
120
+ FastlaneCore::ConfigItem.new(key: :api_token,
121
+ env_name: "LOKALISE_API_TOKEN",
122
+ description: "API Token for Lokalise",
123
+ verify_block: proc do |value|
124
+ UI.user_error! "No API token for Lokalise given, pass using `api_token: 'token'`" unless (value and not value.empty?)
125
+ end),
126
+ FastlaneCore::ConfigItem.new(key: :snapshot_version,
127
+ env_name: "LOKALISE_SNAPSHOT_VERSION",
128
+ description: "snapshot version for Lokalise",
129
+ verify_block: proc do |value|
130
+ UI.user_error! "No API token for Lokalise given, pass using `api_token: 'token'`" unless (value and not value.empty?)
131
+ end),
132
+ FastlaneCore::ConfigItem.new(key: :project_identifier,
133
+ env_name: "LOKALISE_PROJECT_ID",
134
+ description: "Lokalise Project ID",
135
+ verify_block: proc do |value|
136
+ UI.user_error! "No Project Identifier for Lokalise given, pass using `project_identifier: 'identifier'`" unless (value and not value.empty?)
137
+ end),
138
+ FastlaneCore::ConfigItem.new(key: :sources,
139
+ description: "Localization sources",
140
+ is_string: false,
141
+ verify_block: proc do |value|
142
+ UI.user_error! "Tags should be passed as array" unless value.kind_of? Array
143
+ end),
144
+ FastlaneCore::ConfigItem.new(key: :languages,
145
+ description: "Include only the languages",
146
+ optional: true,
147
+ is_string: false,
148
+ default_value: ["en"],
149
+ verify_block: proc do |value|
150
+ UI.user_error! "Tags should be passed as array" unless value.kind_of? Array
151
+ end),
152
+
153
+ ]
154
+ end
155
+
156
+ def self.authors
157
+ "teanet"
158
+ end
159
+
160
+ def self.is_supported?(platform)
161
+ [:ios, :mac].include? platform
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,157 @@
1
+ require 'credentials_manager/account_manager'
2
+ require 'fastlane_core/itunes_transporter'
3
+ require 'rexml/document'
4
+ # a = CredentialsManager::AccountManager.new(user: user, prefix: "deliver.appspecific", note: "application-specific")
5
+ # @password = a.password(ask_if_missing: true) # to ask the user for the missing value
6
+
7
+ module Fastlane
8
+ module Actions
9
+ class PreviewsAction < Action
10
+ def self.run(params)
11
+ user = params[:username]
12
+ product_bundle_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
13
+ spaceship = Spaceship::Tunes.login(user)
14
+ spaceship.team_id = CredentialsManager::AppfileConfig.try_fetch_value(:itc_team_id)
15
+ app = Spaceship::Tunes::Application.find(product_bundle_identifier)
16
+ app_id = app.apple_id()
17
+ transporter = FastlaneCore::ItunesTransporter.new(user)
18
+ destination = "/tmp"
19
+ itmsp_path = File.join(destination, "#{app_id}.itmsp")
20
+ transporter.download(app_id, destination)
21
+ patch_itmsp(itmsp_path)
22
+ transporter.upload(app_id, destination)
23
+ end
24
+
25
+ def self.patch_itmsp(itmsp_path)
26
+ metadata_path = File.join(itmsp_path, "metadata.xml")
27
+ doc = REXML::Document.new(File.read(metadata_path))
28
+ current_version = doc.root.elements["software/software_metadata/versions[1]/version"]
29
+
30
+ software_metadata = doc.root.elements["software/software_metadata"]
31
+ software_metadata.elements.delete("products")
32
+ software_metadata.elements.delete("in_app_purchases")
33
+ versions = doc.root.elements["software/software_metadata/versions"]
34
+
35
+ old_version = versions.elements[2]
36
+ if old_version
37
+ puts "Romove old version"
38
+ versions.elements.delete(old_version)
39
+ end
40
+
41
+ build_folder = File.join(Dir.pwd, "build")
42
+ preview_path = File.join(build_folder, "previews")
43
+
44
+ current_version.elements.each("locales/locale") do |element|
45
+
46
+ element.elements.delete("app_previews")
47
+ element.elements.delete("software_screenshots")
48
+ element.elements.delete("title")
49
+ element.elements.delete("subtitle")
50
+ element.elements.delete("description")
51
+ element.elements.delete("version_whats_new")
52
+ element.elements.delete("privacy_url")
53
+ element.elements.delete("support_url")
54
+ element.elements.delete("keywords")
55
+
56
+ locale = element.attributes["name"]
57
+ locale_path = File.join(preview_path, locale)
58
+ # если нет папки с локалью, значит удаляем видео
59
+ app_previews = REXML::Element.new('app_previews')
60
+ if File.directory?(locale_path)
61
+ locale_files = Dir.entries(locale_path).select {|f| not File.directory?(f) }
62
+
63
+ timestamp = "00:00:08:00"
64
+ for file in locale_files do
65
+ if file == "timestamp.txt"
66
+ timestamp = File.read(File.join(locale_path, file))
67
+ end
68
+ end
69
+ for file in locale_files do
70
+ video_path = File.join(locale_path, file)
71
+ if File.extname(file).downcase == ".mp4"
72
+ size = File.size(video_path)
73
+ if size > 500000
74
+ app_preview = generate_app_preview(itmsp_path, locale, video_path, timestamp)
75
+ app_previews.elements.add(app_preview)
76
+ else
77
+ puts "#{video_path} too small, #{size}b"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ element.elements.add(app_previews)
83
+ end
84
+ formatter = REXML::Formatters::Pretty.new
85
+ formatter.compact = true
86
+ formatter.width = 9999
87
+ File.open(metadata_path,"w"){|file| file.puts formatter.write(doc.root,"")}
88
+ end
89
+
90
+ def self.generate_app_preview(itmsp_path, locale, video_path, timestamp)
91
+ file = last = File.basename(video_path)
92
+ file_with_locale = "#{locale}_#{file}"
93
+ itmsp_video_path = File.join(itmsp_path, file_with_locale)
94
+ FileUtils.cp(video_path, itmsp_video_path)
95
+ file_name = File.basename(file, File.extname(file))
96
+
97
+ app_preview = REXML::Element.new('app_preview')
98
+ app_preview.add_attribute(REXML::Attribute.new('display_target', file_name))
99
+ app_preview.add_attribute(REXML::Attribute.new('position', '1'))
100
+
101
+ data_file = REXML::Element.new('data_file')
102
+ data_file.add_attribute(REXML::Attribute.new('role', 'source'))
103
+
104
+ size_node = REXML::Element.new('size')
105
+ size = File.size(video_path)
106
+ size_node.add_text("#{size}")
107
+ data_file.elements.add(size_node)
108
+
109
+ file_name_node = REXML::Element.new('file_name')
110
+ file_name_node.add_text(file_with_locale)
111
+ data_file.elements.add(file_name_node)
112
+
113
+ checksum = REXML::Element.new('checksum')
114
+ checksum.add_text(Digest::MD5.file(video_path).hexdigest)
115
+ data_file.elements.add(checksum)
116
+
117
+ app_preview.elements.add(data_file)
118
+
119
+ preview_image_time = REXML::Element.new('preview_image_time')
120
+ preview_image_time.add_attribute(REXML::Attribute.new('format', "30/1:1/nonDrop"))
121
+ preview_image_time.add_text(timestamp)
122
+ app_preview.elements.add(preview_image_time)
123
+
124
+ app_preview
125
+ end
126
+
127
+ #####################################################
128
+ # @!group Documentation
129
+ #####################################################
130
+
131
+ def self.description
132
+ "Download Lokalise localization"
133
+ end
134
+
135
+ def self.available_options
136
+ [
137
+ FastlaneCore::ConfigItem.new(
138
+ key: :username,
139
+ env_name: "PREVIEW_USER_NAME",
140
+ description: "User",
141
+ verify_block: proc do |value|
142
+ UI.user_error! "No API token for Lokalise given, pass using `api_token: 'token'`" unless (value and not value.empty?)
143
+ end
144
+ ),
145
+ ]
146
+ end
147
+
148
+ def self.authors
149
+ "teanet"
150
+ end
151
+
152
+ def self.is_supported?(platform)
153
+ [:ios].include? platform
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,142 @@
1
+ import Foundation
2
+
3
+ public final class Api {
4
+
5
+ public struct NoContent: Decodable {
6
+ init() {}
7
+ }
8
+
9
+ public enum ApiError: Error {
10
+ case nilDataError
11
+ case pathError
12
+ case repeatCountLimitReached
13
+ }
14
+
15
+ enum Method: String {
16
+ case get = "GET"
17
+ case post = "POST"
18
+ case put = "PUT"
19
+ }
20
+
21
+ private let session = URLSession(configuration: URLSessionConfiguration.default)
22
+ private let baseURL: URL
23
+
24
+ public init(baseURL: String) {
25
+ self.baseURL = URL(string: baseURL)!
26
+ }
27
+
28
+ public func get<T: Codable>(
29
+ path: String,
30
+ query: [String: String] = [:],
31
+ headers: [String: String] = [:],
32
+ timeoutInterval: TimeInterval
33
+ ) throws -> T {
34
+ return try self.method(.get, path: path, query: query, headers: headers, timeoutInterval: timeoutInterval)
35
+ }
36
+
37
+ public func post<T: Codable, TBody: Encodable>(
38
+ path: String,
39
+ body: TBody,
40
+ query: [String: String] = [:],
41
+ headers: [String: String] = [:],
42
+ timeoutInterval: TimeInterval
43
+ ) throws -> T {
44
+ let body = try JSONEncoder().encode(body)
45
+ return try self.method(.post, path: path, query: query, headers: headers, timeoutInterval: timeoutInterval, body: body)
46
+ }
47
+
48
+ private func get<T: Codable>(path: String, completion: @escaping (Result<T, Error>) -> Void) {
49
+ self.method(.get, path: path, completion: completion)
50
+ }
51
+
52
+ private func post<T: Codable, TBody: Encodable>(
53
+ path: String,
54
+ body: TBody, headers: [String: String] = [:],
55
+ completion: @escaping (Result<T, Error>) -> Void
56
+ ) {
57
+ let body = try? JSONEncoder().encode(body)
58
+ self.method(.post, path: path, headers: headers, body: body, completion: completion)
59
+ }
60
+
61
+ private func method<T: Decodable>(
62
+ _ method: Method,
63
+ path: String,
64
+ query: [String: String] = [:],
65
+ headers: [String: String] = [:],
66
+ timeoutInterval: TimeInterval,
67
+ body: Data? = nil
68
+ ) throws -> T {
69
+ let s = DispatchSemaphore(value: 0)
70
+ var statsResponse: Result<T, Error>!
71
+ let completion: (Result<T, Error>) -> Void = { result in
72
+ statsResponse = result
73
+ s.signal()
74
+ }
75
+ self.method(
76
+ method,
77
+ path: path,
78
+ query: query,
79
+ headers: headers,
80
+ body: body,
81
+ timeoutInterval: timeoutInterval,
82
+ completion: completion
83
+ )
84
+ s.wait()
85
+ return try statsResponse.get()
86
+ }
87
+
88
+ private func method<T: Decodable>(
89
+ _ method: Method,
90
+ path: String,
91
+ query: [String: String] = [:],
92
+ headers: [String: String] = [:],
93
+ body: Data? = nil,
94
+ timeoutInterval: TimeInterval = 60,
95
+ completion: @escaping (Result<T, Error>) -> Void
96
+ ) {
97
+ let baseURL = self.baseURL.appendingPathComponent(path)
98
+ var cmp = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)
99
+ cmp?.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) }
100
+ guard let url = cmp?.url else { completion(.failure(ApiError.pathError)); return }
101
+ var request = URLRequest(url: url)
102
+ request.timeoutInterval = timeoutInterval
103
+ request.httpBody = body
104
+ request.httpMethod = method.rawValue
105
+ var allHTTPHeaderFields = headers
106
+ if headers["content-type"] == nil {
107
+ allHTTPHeaderFields["content-type"] = "application/json"
108
+ }
109
+ request.allHTTPHeaderFields = allHTTPHeaderFields
110
+ print("Start request: \(method.rawValue) \(url)")
111
+ self.session.dataTask(with: request) { (data, _, error) in
112
+
113
+ if let error = error {
114
+ completion(.failure(error))
115
+ return
116
+ }
117
+ guard let data = data else {
118
+ completion(.failure(ApiError.nilDataError))
119
+ return
120
+ }
121
+ if data.isEmpty, let noContent = NoContent() as? T {
122
+ completion(.success(noContent))
123
+ return
124
+ }
125
+
126
+ if let string = String(data: data, encoding: .utf8)?.prefix(3000) {
127
+ print("Finish: \(string)...")
128
+ } else {
129
+ print("Finish")
130
+ }
131
+
132
+ do {
133
+ let response = try JSONDecoder().decode(T.self, from: data)
134
+ completion(.success(response))
135
+ } catch {
136
+ print("Decode response \(url) error: \(error)")
137
+ completion(.failure(error))
138
+ }
139
+ }.resume()
140
+ }
141
+
142
+ }