Class: Deliver::AppMetadata

Inherits:
Object
  • Object
show all
Defined in:
lib/deliver/app_metadata.rb,
lib/deliver/app_metadata_screenshots.rb

Direct Known Subclasses

IpaUploader

Constant Summary collapse

ITUNES_NAMESPACE =
"http://apple.com/itunes/importer"
INVALID_LANGUAGE_ERROR =
"The specified language could not be found. Make sure it is available in Deliver::Languages::ALL_LANGUAGES"

Instance Attribute Summary collapse

Updating metadata information collapse

Manually fetching elements from the metadata.xml collapse

Uploading the updated metadata collapse

Screenshot related collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, dir, redownload_package = true) ⇒ AppMetadata

You don’t have to manually create an AppMetadata object. It will be created when you access the app’s metadata (Deliver::App#metadata)

Parameters:

  • app (Deliver::App)

    The app this metadata is from/for

  • dir (String)

    The app this metadata is from/for

  • redownload_package (bool) (defaults to: true)

    When true the current package will be downloaded from iTC before you can modify any values. This should only be false for unit tests

Raises:



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/deliver/app_metadata.rb', line 49

def initialize(app, dir, redownload_package = true)
  raise AppMetadataParameterError.new("No valid Deliver::App given") unless app.kind_of?Deliver::App

  @metadata_dir = dir
  @app = app

  if self.class == AppMetadata
    if redownload_package
      # Delete the one that may exists already
      unless Helper.is_test?
        `rm -rf #{dir}/*.itmsp`
      end

      # we want to update the metadata, so first we have to download the existing one
      transporter.download(app, dir)

      # Parse the downloaded package
      parse_package(dir)
    else
      # use_data contains the data to be used. This is the case for unit tests
      parse_package(dir)
    end
  end
end

Instance Attribute Details

#informationObject

Returns Data contains all information for this app, including the unmodified one.

Returns:

  • Data contains all information for this app, including the unmodified one



18
19
20
# File 'lib/deliver/app_metadata.rb', line 18

def information
  @information
end

Instance Method Details

#add_new_locale(language) ⇒ Bool

Adds a new locale (language) to the given app

Parameters:

Returns:

  • (Bool)

    Is true, if the language was created. False, when the language alreade existed

Raises:



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/deliver/app_metadata.rb', line 90

def add_new_locale(language)
  unless Deliver::Languages::ALL_LANGUAGES.include?language
    raise "Language '#{language}' is invalid. It must be in #{Deliver::Languages::ALL_LANGUAGES}."
  end

  if information[language] != nil
    Helper.log.info("Locale '#{language}' already exists. Can not create it again.")
    return false
  end


  locales = fetch_value("//x:locales").first

  new_locale = @data.create_element('locale')
  new_locale['name'] = language
  locales << new_locale

  # Title is the only thing which is required by iTC
  default_title = information.values.first[:title][:value]

  title = @data.create_element('title')
  title.content = default_title
  new_locale << title

  Helper.log.info("Successfully created the new locale '#{language}'. The default title '#{default_title}' was set, since it's required by iTunesConnect.")
  Helper.log.info("You can update the title using 'app.metadata.update_title'")

  information[language] ||= {}
  information[language][:title] = { value: default_title, modified: true}

  true
end

#add_screenshot(language, app_screenshot) ⇒ Object

Appends another screenshot to the already existing ones

Parameters:

  • language (String)

    The language, which has to be in this list: Languages.

  • app_screenshot (Deliver::AppScreenshot)

    The screenshot you want to add to the app metadata.

Raises:



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/deliver/app_metadata_screenshots.rb', line 24

def add_screenshot(language, app_screenshot)
  raise AppMetadataParameterError.new(INVALID_LANGUAGE_ERROR) unless Languages::ALL_LANGUAGES.include?language

  create_locale_if_not_exists(language)

  # Fetch the 'software_screenshots' node (array) for the specific locale
  locales = self.fetch_value("//x:locale[@name='#{language}']")

  screenshots = self.fetch_value("//x:locale[@name='#{language}']/x:software_screenshots").first

  if not screenshots or screenshots.children.count == 0
    screenshots.remove if screenshots

    # First screenshot ever
    screenshots = Nokogiri::XML::Node.new('software_screenshots', @data)
    locales.first << screenshots

    node_set = Nokogiri::XML::NodeSet.new(@data)
    node_set << app_screenshot.create_xml_node(@data, 1)
    screenshots.children = node_set
  else
    # There is already at least one screenshot
    next_index = 1
    screenshots.children.each do |screen|
      if screen['display_target'] == app_screenshot.screen_size
        next_index += 1
      end
    end

    if next_index > MAXIMUM_NUMBER_OF_SCREENSHOTS
      raise AppMetadataTooManyScreenshotsError.new("Only #{MAXIMUM_NUMBER_OF_SCREENSHOTS} screenshots are allowed per language per device type (#{app_screenshot.screen_size})")
    end

    # Ready for storing the screenshot into the metadata.xml now
    screenshots.children.after(app_screenshot.create_xml_node(@data, next_index))
  end

  information[language][:screenshots] << app_screenshot

  app_screenshot.store_file_inside_package(@package_path)
end

#clear_all_screenshots(language) ⇒ Object

Removes all currently enabled screenshots for the given language.

Parameters:

  • language (String)

    The language, which has to be in this list: Languages.

Raises:



9
10
11
12
13
14
15
16
17
# File 'lib/deliver/app_metadata_screenshots.rb', line 9

def clear_all_screenshots(language)
  raise AppMetadataParameterError.new(INVALID_LANGUAGE_ERROR) unless Languages::ALL_LANGUAGES.include?language

  update_localized_value('software_screenshots', {language => {}}) do |field, useless, language|
    field.children.remove # remove all the screenshots
  end
  information[language][:screenshots] = []
  true
end

#fetch_value(xpath) ⇒ Object

Directly fetch XML nodes from the metadata.xml.

Examples:

Fetch all keywords

fetch_value("//x:keyword")

Fetch a specific locale

fetch_value("//x:locale[@name='de-DE']")

Fetch the node that contains all screenshots for a specific language

fetch_value("//x:locale[@name='de-DE']/x:software_screenshots")

Returns:

  • the requests XML nodes or node set



224
225
226
# File 'lib/deliver/app_metadata.rb', line 224

def fetch_value(xpath)
  @data.xpath(xpath, "x" => ITUNES_NAMESPACE)
end

#set_all_screenshots(new_screenshots) ⇒ bool

This method will clear all screenshots and set the new ones you pass This method uses #clear_all_screenshots and #add_screenshot under the hood.

Parameters:

Returns:

  • (bool)

    true if everything was successful

Raises:



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/deliver/app_metadata_screenshots.rb', line 79

def set_all_screenshots(new_screenshots)
  error_text = "Please pass a hash, containing an array of AppScreenshot objects"
  raise AppMetadataParameterError.new(error_text) unless new_screenshots.kind_of?Hash

  new_screenshots.each do |key, value|
    if key.kind_of?String and value.kind_of?Array and value.count > 0 and value.first.kind_of?AppScreenshot

      self.clear_all_screenshots(key)

      value.each do |screen|
        add_screenshot(key, screen)
      end
    else
      raise AppMetadataParameterError.new(error_text)
    end
  end
  true
end

#set_all_screenshots_from_path(path) ⇒ Object

This method will run through all the available locales, check if there is a folder for this language (e.g. ‘en-US’) and use all screenshots in there

Parameters:

  • path (String)

    A path to the folder, which contains a folder for each locale

Raises:



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/deliver/app_metadata_screenshots.rb', line 137

def set_all_screenshots_from_path(path)
  raise AppMetadataParameterError.new("Parameter needs to be a path (string)") unless path.kind_of?String

  found = false
  Deliver::Languages::ALL_LANGUAGES.each do |language|
    full_path = path + "/#{language}"
    if File.directory?(full_path)
      found = true
      set_screenshots_for_each_language({
        language => full_path
      })
    end
  end
  return found
end

#set_screenshots_for_each_language(hash) ⇒ Object

Automatically add all screenshots contained in the given directory to the app.

This method will automatically detect which device type each screenshot is.

This will also clear all existing screenshots before setting the new ones.

Parameters:

Raises:



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/deliver/app_metadata_screenshots.rb', line 104

def set_screenshots_for_each_language(hash)
  raise AppMetadataParameterError.new("Parameter needs to be an hash, containg strings with the new description") unless hash.kind_of?Hash

  hash.each do |language, current_path|
    resulting_path = "#{current_path}/**/*.{png,jpg,jpeg}"

    raise AppMetadataParameterError.new(INVALID_LANGUAGE_ERROR) unless Languages::ALL_LANGUAGES.include?language

    # https://stackoverflow.com/questions/21688855/
    # File::FNM_CASEFOLD = ignore case
    if Dir.glob(resulting_path, File::FNM_CASEFOLD).count == 0
      Helper.log.error("No screenshots found at the given path '#{resulting_path}'")
    else
      self.clear_all_screenshots(language)

      Dir.glob(resulting_path, File::FNM_CASEFOLD).sort.each do |path|
        next if path.include?"_framed."

        begin
          add_screenshot(language, Deliver::AppScreenshot.new(path))
        rescue AppMetadataTooManyScreenshotsError => ex
          # We just use the first 5 ones
        end
      end
    end
  end

  true
end

#update_changelog(hash) ⇒ Object

Updates the app changelog of the latest version

Parameters:

  • hash (Hash)

    The hash should contain the correct language codes (Languages) as keys.

Raises:



148
149
150
# File 'lib/deliver/app_metadata.rb', line 148

def update_changelog(hash)
  (:version_whats_new, hash)
end

#update_description(hash) ⇒ Object

Updates the app description which is shown in the AppStore

Parameters:

  • hash (Hash)

    The hash should contain the correct language codes (Languages) as keys.

Raises:



140
141
142
# File 'lib/deliver/app_metadata.rb', line 140

def update_description(hash)
  (:description, hash)
end

#update_keywords(hash) ⇒ Object

Updates the app keywords

Parameters:

  • hash (Hash)

    The hash should contain the correct language codes (Languages) as keys. The value should be an array of keywords (each keyword is a string)

Raises:



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/deliver/app_metadata.rb', line 180

def update_keywords(hash)
  update_localized_value('keywords', hash) do |field, keywords, language|
    raise AppMetadataParameterError.new("Parameter needs to be a hash (each language) with an array of keywords in it (given: #{hash})") unless keywords.kind_of?Array

    if not information[language][:keywords] or keywords.sort != information[language][:keywords][:value].sort
      field.children.remove # remove old keywords

      node_set = Nokogiri::XML::NodeSet.new(@data)
      keywords.each do |word|
        keyword = Nokogiri::XML::Node.new('keyword', @data)
        keyword.content = word
        node_set << keyword
      end

      field.children = node_set

      information[language][:keywords] = { value: keywords, modified: true }
    end
  end
end

#update_marketing_url(hash) ⇒ Object

Updates the Marketing URL

Parameters:

  • hash (Hash)

    The hash should contain the correct language codes (Languages) as keys.

Raises:



156
157
158
# File 'lib/deliver/app_metadata.rb', line 156

def update_marketing_url(hash)
  (:software_url, hash)
end

#update_price_tier(tier) ⇒ Object

Updates the price tier of the given app

Parameters:

  • tier (Integer)

    The tier that should be used from now on



203
204
205
206
207
208
209
210
# File 'lib/deliver/app_metadata.rb', line 203

def update_price_tier(tier)
  raise "Price Tier '#{tier}' must be of type integer".red unless tier.kind_of?Integer
  raise "Invalid price tier '#{tier}' given, must be 0 to 94".red unless (tier.to_i >= 0 and tier.to_i <= 87)

  price = fetch_value("//x:wholesale_price_tier").last
  raise "No initial pricing found, please set the pricing at least once on iTunes Connect.".red unless price
  price.content = tier
end

#update_privacy_url(hash) ⇒ Object

Updates the Privacy URL

Parameters:

  • hash (Hash)

    The hash should contain the correct language codes (Languages) as keys.

Raises:



172
173
174
# File 'lib/deliver/app_metadata.rb', line 172

def update_privacy_url(hash)
  (:privacy_url, hash)
end

#update_support_url(hash) ⇒ Object

Updates the Support URL

Parameters:

  • hash (Hash)

    The hash should contain the correct language codes (Languages) as keys.

Raises:



164
165
166
# File 'lib/deliver/app_metadata.rb', line 164

def update_support_url(hash)
  (:support_url, hash)
end

#update_title(hash) ⇒ Object

Updates the app title

Parameters:

  • hash (Hash)

    The hash should contain the correct language codes (Languages) as keys.

Raises:



132
133
134
# File 'lib/deliver/app_metadata.rb', line 132

def update_title(hash)
  (:title, hash)
end

#upload!Object

Actually uploads the updated metadata to Apple. This method might take a while.

Raises:



237
238
239
240
241
242
243
244
# File 'lib/deliver/app_metadata.rb', line 237

def upload!
  unless Helper.is_test?
    # First: Write the current XML state to disk
    File.write("#{@package_path}/#{METADATA_FILE_NAME}", @data.to_xml)
  end

  transporter.upload(@app, @metadata_dir)
end

#verify_version(version_number) ⇒ Object

Verifies the if the version of iTunesConnect matches the one you pass as parameter



79
80
81
82
83
# File 'lib/deliver/app_metadata.rb', line 79

def verify_version(version_number)
  xml_version = self.fetch_value("//x:version").first['string']
  raise "Version mismatch: on iTunesConnect the latest version is '#{xml_version}', you specified '#{version_number}'" if xml_version != version_number
  true
end