ETLane 0.1.42 → 0.1.46

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.
@@ -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
+ }