class Spaceship::Tunes::AppVersion
rubocop:disable Metrics/ClassLength
This can either be the live or the edit version retrieved via the app
Represents an editable version of an iTunes Connect Application
def candidate_builds
def candidate_builds res = client.candidate_builds(self.application.apple_id, self.version_id) builds = [] res.each do |attrs| next unless attrs["type"] == "BUILD" # I don't know if it can be something else. builds << Tunes::Build.factory(attrs) end return builds end
def check_preview_screenshot_resolution(preview_screenshot_path, device)
def check_preview_screenshot_resolution(preview_screenshot_path, device) is_portrait = Utilities.portrait?(preview_screenshot_path) expected_resolution = TunesClient.video_preview_resolution_for(device, is_portrait) actual_resolution = Utilities.resolution(preview_screenshot_path) orientation = is_portrait ? "portrait" : "landscape" raise "Invalid #{orientation} screenshot resolution for device #{device}. Should be #{expected_resolution}" unless (actual_resolution == expected_resolution) end
def container_data_for_language_and_device(data_field, language, device)
def container_data_for_language_and_device(data_field, language, device) raise "#{device} isn't a valid device name" unless DeviceType.exists?(device) languages = raw_data_details.select { |d| d["language"] == language } # IDEA: better error for non existing language raise "#{language} isn't an activated language" unless languages.count > 0 lang_details = languages[0] devices_details = lang_details[data_field]["value"] raise "Unexpected state: missing device details for #{device}" unless devices_details.key? device devices_details[device] end
def create_languages(languages)
This will create the new language if it's not available yet and do nothing if everything's there
You should call this method before accessing the name, description and other localized values
Call this method to make sure the given languages are available for this app
def create_languages(languages) languages = [languages] if languages.kind_of?(String) raise "Please pass an array" unless languages.kind_of? Array copy_from = self.languages.find { |a| a['language'] == 'en-US' } || self.languages.first languages.each do |language| # First, see if it's already available found = self.languages.find do |local| local['language'] == language end next if found new_language = copy_from.clone new_language['language'] = language self.languages << new_language end nil end
def factory(attrs)
Create a new object based on a hash.
def factory(attrs) obj = self.new(attrs) obj.unfold_languages return obj end
def find(application, app_id, is_live)
-
is_live
(Boolean
) -- -
app_id
(String
) -- The unique Apple ID of this app -
application
(Spaceship::Tunes::Application
) -- The app this version is for
def find(application, app_id, is_live) attrs = client.app_version(app_id, is_live) return nil unless attrs attrs.merge!(application: application) attrs.merge!(is_live: is_live) return self.factory(attrs) end
def is_live?
-
(Bool)
- Is that version currently available in the App Store?
def is_live? is_live end
def raw_data_details
def raw_data_details raw_data["details"]["value"] end
def release_on_approval
These methods takes care of properly parsing values that
def release_on_approval super == 'true' end
def save!
def save! client.update_app_version!(application.apple_id, self.version_id, raw_data) end
def screenshots_data_for_language_and_device(language, device)
def screenshots_data_for_language_and_device(language, device) container_data_for_language_and_device("screenshots", language, device) end
def select_build(build)
You have to pass a build you got from - candidate_builds
Select a build to be submitted for Review.
def select_build(build) raw_data.set(['preReleaseBuildVersionString', 'value'], build.build_version) raw_data.set(['preReleaseBuildTrainVersionString'], build.train_version) raw_data.set(['preReleaseBuildUploadDate'], build.upload_date) true end
def setup
def setup # Properly parse the AppStatus status = raw_data['status'] @app_status = Tunes::AppStatus.get_from_string(status) setup_large_app_icon setup_watch_app_icon setup_transit_app_file setup_screenshots setup_trailers end
def setup_large_app_icon
def setup_large_app_icon large_app_icon = raw_data["largeAppIcon"]["value"] @large_app_icon = nil @large_app_icon = Tunes::AppImage.factory(large_app_icon) if large_app_icon end
def setup_screenshots
def setup_screenshots @screenshots = {} raw_data_details.each do |row| # Now that's one language right here @screenshots[row['language']] = setup_screenshots_for(row) end end
def setup_screenshots_for(row)
def setup_screenshots_for(row) screenshots = row.fetch("screenshots", {}).fetch("value", nil) return [] unless screenshots result = [] screenshots.each do |device_type, value| value["value"].each do |screenshot| screenshot_data = screenshot["value"] data = { device_type: device_type, language: row["language"] }.merge(screenshot_data) result << Tunes::AppScreenshot.factory(data) end end return result end
def setup_trailers
def setup_trailers @trailers = {} raw_data_details.each do |row| # Now that's one language right here @trailers[row["language"]] = setup_trailers_for(row) end end
def setup_trailers_for(row)
def setup_trailers_for(row) trailers = row.fetch("appTrailers", {}).fetch("value", nil) return [] unless trailers result = [] trailers.each do |device_type, value| trailer_data = value["value"] next if trailer_data.nil? data = { device_type: device_type, language: row["language"] }.merge(trailer_data) result << Tunes::AppTrailer.factory(data) end return result end
def setup_transit_app_file
def setup_transit_app_file transit_app_file = raw_data["transitAppFile"]["value"] @transit_app_file = nil @transit_app_file = Tunes::TransitAppFile.factory(transit_app_file) if transit_app_file end
def setup_watch_app_icon
def setup_watch_app_icon watch_app_icon = raw_data["watchAppIcon"]["value"] @watch_app_icon = nil @watch_app_icon = Tunes::AppImage.factory(watch_app_icon) if watch_app_icon end
def supports_apple_watch
def supports_apple_watch !super.nil? end
def trailer_data_for_language_and_device(language, device)
def trailer_data_for_language_and_device(language, device) container_data_for_language_and_device("appTrailers", language, device) end
def unfold_languages
def unfold_languages { keywords: :keywords, description: :description, supportURL: :support_url, marketingURL: :marketing_url, releaseNotes: :release_notes }.each do |json, attribute| instance_variable_set("@#{attribute}".to_sym, LanguageItem.new(json, languages)) end end
def update_rating(hash)
Available Values
})
'GAMBLING_CONTESTS' => 0
'UNRESTRICTED_WEB_ACCESS' => 0,
'MATURE_SUGGESTIVE' => 2,
'CARTOON_FANTASY_VIOLENCE' => 0,
v.update_rating({
Call it like this:
Set the age restriction rating
def update_rating(hash) raise "Must be a hash" unless hash.kind_of?(Hash) hash.each do |key, value| to_edit = self.raw_data['ratings']['nonBooleanDescriptors'].find do |current| current['name'].include?(key) end if to_edit to_set = "NONE" if value == 0 to_set = "INFREQUENT_MILD" if value == 1 to_set = "FREQUENT_INTENSE" if value == 2 raise "Invalid value '#{value}' for '#{key}', must be 0-2" unless to_set to_edit['level'] = "ITC.apps.ratings.level.#{to_set}" else # Maybe it's a boolean descriptor? to_edit = self.raw_data['ratings']['booleanDescriptors'].find do |current| current['name'].include?(key) end if to_edit to_set = "NO" to_set = "YES" if value.to_i > 0 to_edit['level'] = "ITC.apps.ratings.level.#{to_set}" else raise "Could not find option '#{key}' in the list of available options" end end end true end
def upload_geojson!(geojson_path)
-
icon_path
(String
) -- : The path to the geojson file. Use nil to remove it
def upload_geojson!(geojson_path) unless geojson_path raw_data["transitAppFile"]["value"] = nil @transit_app_file = nil return end upload_file = UploadFile.from_path geojson_path geojson_data = client.upload_geojson(self, upload_file) @transit_app_file = Tunes::TransitAppFile.factory({}) if @transit_app_file.nil? @transit_app_file .url = nil # response.headers['Location'] @transit_app_file.asset_token = geojson_data["token"] @transit_app_file.name = upload_file.file_name @transit_app_file.time_stamp = Time.now.to_i * 1000 # works without but... end
def upload_large_icon!(icon_path)
-
icon_path
(String
) -- : The path to the icon. Use nil to remove it
def upload_large_icon!(icon_path) unless icon_path @large_app_icon.reset! return end upload_image = UploadFile.from_path icon_path image_data = client.upload_large_icon(self, upload_image) @large_app_icon.reset!({ asset_token: image_data['token'], original_file_name: upload_image.file_name }) end
def upload_screenshot!(screenshot_path, sort_order, language, device)
-
device
(string
) -- : The device for this screenshot -
language
(String
) -- : The language for this screenshot -
sort_order
(Fixnum
) -- : The sort_order, from 1 to 5 -
icon_path
(String
) -- : The path to the screenshot. Use nil to remove it
def upload_screenshot!(screenshot_path, sort_order, language, device) raise "sort_order must be positive" unless sort_order > 0 raise "sort_order must not be > 5" if sort_order > 5 # this will also check both language and device parameters device_lang_screenshots = screenshots_data_for_language_and_device(language, device)["value"] existing_sort_orders = device_lang_screenshots.map { |s| s["value"]["sortOrder"] } if screenshot_path # adding / replacing upload_file = UploadFile.from_path screenshot_path screenshot_data = client.upload_screenshot(self, upload_file, device) new_screenshot = { "value" => { "assetToken" => screenshot_data["token"], "sortOrder" => sort_order, "url" => nil, "thumbNailUrl" => nil, "originalFileName" => upload_file.file_name } } if existing_sort_orders.include?(sort_order) # replace device_lang_screenshots[existing_sort_orders.index(sort_order)] = new_screenshot else # add device_lang_screenshots << new_screenshot end else # removing raise "cannot remove screenshot with non existing sort_order" unless existing_sort_orders.include?(sort_order) device_lang_screenshots.delete_at(existing_sort_orders.index(sort_order)) end setup_screenshots end
def upload_trailer!(trailer_path, language, device, timestamp = "05.00", preview_image_path = nil)
-
timestamp
(String
) -- : The optional timestamp of the screenshot to grab -
device
(String
) -- : The device for this screenshot -
language
(String
) -- : The language for this screenshot -
sort_order
(Fixnum
) -- : The sort_order, from 1 to 5 -
icon_path
(String
) -- : The path to the screenshot. Use nil to remove it
def upload_trailer!(trailer_path, language, device, timestamp = "05.00", preview_image_path = nil) raise "No app trailer supported for iphone35" if device == 'iphone35' device_lang_trailer = trailer_data_for_language_and_device(language, device) if trailer_path # adding / replacing trailer / replacing preview raise "Invalid timestamp #{timestamp}" if (timestamp =~ /^[0-9][0-9].[0-9][0-9]$/).nil? if preview_image_path check_preview_screenshot_resolution(preview_image_path, device) video_preview_path = preview_image_path else # IDEA: optimization, we could avoid fetching the screenshot if the timestamp hasn't changed video_preview_resolution = video_preview_resolution_for(device, trailer_path) video_preview_path = Utilities.grab_video_preview(trailer_path, timestamp, video_preview_resolution) end video_preview_file = UploadFile.from_path video_preview_path video_preview_data = client.upload_trailer_preview(self, video_preview_file) trailer = device_lang_trailer["value"] if trailer.nil? # add trailer upload_file = UploadFile.from_path trailer_path trailer_data = client.upload_trailer(self, upload_file) trailer_data = trailer_data['responses'][0] trailer = { "videoAssetToken" => trailer_data["token"], "descriptionXML" => trailer_data["descriptionDoc"], "contentType" => upload_file.content_type } device_lang_trailer["value"] = trailer end # add / update preview # different format required ts = "00:00:#{timestamp}" ts[8] = ':' trailer.merge!({ "pictureAssetToken" => video_preview_data["token"], "previewFrameTimeCode" => "#{ts}", "isPortrait" => Utilities.portrait?(video_preview_path) }) else # removing trailer raise "cannot remove non existing trailer" if device_lang_trailer["value"].nil? device_lang_trailer["value"] = nil end setup_trailers end
def upload_watch_icon!(icon_path)
-
icon_path
(String
) -- : The path to the icon. Use nil to remove it
def upload_watch_icon!(icon_path) unless icon_path @watch_app_icon.reset! return end upload_image = UploadFile.from_path icon_path image_data = client.upload_watch_icon(self, upload_image) @watch_app_icon.reset!({ asset_token: image_data["token"], original_file_name: upload_image.file_name }) end
def url
-
(String)
- An URL to this specific resource. You can enter this URL into your browser
def url url = "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app/904332168/ios/versioninfo/" url += "deliverable" if self.is_live? return url end
def video_preview_resolution_for(device, video_path)
def video_preview_resolution_for(device, video_path) is_portrait = Utilities.portrait?(video_path) TunesClient.video_preview_resolution_for(device, is_portrait) end