motion-steward 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/motion-steward +134 -0
- data/lib/motion-steward/app_store_research.rb +94 -0
- data/lib/motion-steward/app_store_search.rb +80 -0
- data/lib/motion-steward.rb +8 -0
- metadata +70 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: adbe9735dc7992626d91305de5fe4f802a3499cb
|
4
|
+
data.tar.gz: f340890903488d8d0234df95c232a8d62733e8df
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ad68238fb4e2d9a3ef4374cfa5f732fc81404a832654080237298fa3c9e7e3451c0bf0c6cdbec24c94953c20d2e3b2f01865094d50ab7248fa8c09cd4cfc747e
|
7
|
+
data.tar.gz: b9b10c6d7190fd8264f81b500ba988bb4073bbd4f513e544def24be9ddb1650090b57cf2016832e80fae8d3edbbd09642c712dcef2484f4ab32f9c060c25f2f8
|
data/bin/motion-steward
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fastlane'
|
4
|
+
require 'httparty'
|
5
|
+
require 'motion-steward'
|
6
|
+
|
7
|
+
def options
|
8
|
+
[:research]
|
9
|
+
end
|
10
|
+
|
11
|
+
def list_apps
|
12
|
+
Spaceship.app.all.map do |a|
|
13
|
+
puts "#{a.name}: #{a.bundle_id}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def go_to_apps
|
18
|
+
puts 'opening https://developer.apple.com/account/ios/identifier/bundle/'
|
19
|
+
system 'open https://developer.apple.com/account/ios/identifier/bundle/'
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_udid_of_connected_devices
|
23
|
+
script = <<~SCRIPT
|
24
|
+
i=0
|
25
|
+
for line in $(system_profiler SPUSBDataType | sed -n -e '/iPad/,/Serial/p' -e '/iPhone/,/Serial/p' | grep "Serial Number:" | awk -F ": " '{print $2}'); do
|
26
|
+
UDID=${line}
|
27
|
+
echo $UDID
|
28
|
+
udid_array[i]=${line}
|
29
|
+
i=$(($i+1))
|
30
|
+
done
|
31
|
+
|
32
|
+
cnt=${#udid_array[@]}
|
33
|
+
for ((i=0;i<cnt;i++)); do
|
34
|
+
echo ${udid_array[i]}
|
35
|
+
done
|
36
|
+
SCRIPT
|
37
|
+
|
38
|
+
system script
|
39
|
+
end
|
40
|
+
|
41
|
+
def gets
|
42
|
+
STDIN.gets.chomp
|
43
|
+
end
|
44
|
+
|
45
|
+
def prompt_for_response message
|
46
|
+
message += ' ' if message[-1] != ' '
|
47
|
+
print message
|
48
|
+
gets
|
49
|
+
end
|
50
|
+
|
51
|
+
def research
|
52
|
+
term = prompt_for_response "Enter a search term or app name, and I'll return a return what people will see when the search for that term (along with some fancy data):"
|
53
|
+
MotionSteward::AppStoreResearch.analyze(term)
|
54
|
+
end
|
55
|
+
|
56
|
+
def help_with_certificates
|
57
|
+
production = Spaceship.certificate.all.find_all { |c| c.is_a? Spaceship::Portal::Certificate::Production }.first
|
58
|
+
|
59
|
+
development = Spaceship.certificate.all.find_all { |c| c.is_a? Spaceship::Portal::Certificate::Development }.first
|
60
|
+
|
61
|
+
if production.nil? or development.nil?
|
62
|
+
puts "You need to create a Production and Development certificate. This script doesn't support that stuff yet."
|
63
|
+
system "open https://developer.apple.com/account/ios/certificate/"
|
64
|
+
exit(0)
|
65
|
+
end
|
66
|
+
|
67
|
+
[development, production]
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_app
|
71
|
+
puts <<~INSTRUCTIONS
|
72
|
+
I need the name of your app. Here are some tips:
|
73
|
+
|
74
|
+
- If you haven't done so, you should really run the research command before committing to a name for your app.
|
75
|
+
- Using spaces is fine.
|
76
|
+
- Using a hypen after the main app name (eg: "Metal Gear - Tactical Espionage") is fine.
|
77
|
+
- Make the app name memorable because this is what people will use to find your app in the App Store.
|
78
|
+
- Here are examples of BAD app names:
|
79
|
+
- "Threes!" Why? Because it has an exclamation point (punctuation) in the main app name. Apps that use punctuation are hard to find.
|
80
|
+
- "T.L.D.R. Reddit" Why? Because it has periods (punctuation) in the main app name. Apps that use punctuation are hard to find.
|
81
|
+
- "Heroes Fighting" Why? Heroes is a saturated app keyword (there are many others). It'll make your app incredibly difficult to find.
|
82
|
+
|
83
|
+
INSTRUCTIONS
|
84
|
+
|
85
|
+
continue = false
|
86
|
+
while !continue
|
87
|
+
print "Pressure's on, what will you name your app?: "
|
88
|
+
app_name = gets
|
89
|
+
puts ""
|
90
|
+
print 'Given the results above, do you want to change the name of your app? y/n: '
|
91
|
+
continue = true if gets == 'n'
|
92
|
+
end
|
93
|
+
puts 'I need an app website/company website to generate an app ID. Examples: amirrajan.net, rubymotion.com'
|
94
|
+
website = gets
|
95
|
+
recommended_app_id = website.split('.').reverse.join '.'
|
96
|
+
recommended_app_id += '.' + app_name.downcase.delete(' ')
|
97
|
+
app_id = recommended_app_id
|
98
|
+
puts "Alright, here is the app that I'll create for you: "
|
99
|
+
puts app_name
|
100
|
+
puts app_id
|
101
|
+
puts 'Everything look good? y/n: '
|
102
|
+
if gets == 'n'
|
103
|
+
print 'What would you like the App ID to be? '
|
104
|
+
app_id = gets
|
105
|
+
end
|
106
|
+
|
107
|
+
Spaceship.app.create!(bundle_id: app_id, name: app_name)
|
108
|
+
|
109
|
+
puts "If all went well, [#{app_name}] has now been created. Select list_apps to continue."
|
110
|
+
end
|
111
|
+
|
112
|
+
continue = true
|
113
|
+
|
114
|
+
while continue
|
115
|
+
puts ''
|
116
|
+
puts 'Options:'
|
117
|
+
puts ''
|
118
|
+
options.map { |o| puts "- #{o}" }
|
119
|
+
puts '- exit'
|
120
|
+
puts ''
|
121
|
+
|
122
|
+
print 'What would you like to do (choose from the list above): '
|
123
|
+
|
124
|
+
input = gets
|
125
|
+
|
126
|
+
if input == 'exit'
|
127
|
+
exit(0)
|
128
|
+
end
|
129
|
+
|
130
|
+
if options.include? input.to_sym
|
131
|
+
system 'clear'
|
132
|
+
send(input.to_sym)
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'motion-steward/app_store_search'
|
3
|
+
|
4
|
+
class Integer
|
5
|
+
def commas
|
6
|
+
self #=> 12345678
|
7
|
+
.to_s #=> "12345678"
|
8
|
+
.reverse #=> "87654321"
|
9
|
+
.scan(/\d{1,3}/) #=> ["876","543","21"]
|
10
|
+
.join(",") #=> "876,543,21"
|
11
|
+
.reverse #=> "12,345,678"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class MotionSteward::AppStoreResearch
|
16
|
+
def self.analyze name
|
17
|
+
apps = MotionSteward::AppStoreSearch.search_for_app(name)
|
18
|
+
|
19
|
+
if apps.any? { |a| a[:track_name] == name }
|
20
|
+
puts "WARNING: You can't name your app \"#{name}\". Because there is already an app out there with that exact name."
|
21
|
+
end
|
22
|
+
|
23
|
+
apps.each do |a|
|
24
|
+
puts a[:track_name] + " (Category: #{a[:genres].join(', ')}, Price: #{(a[:price] || 0)})"
|
25
|
+
|
26
|
+
release_score = determine_release_score(a)
|
27
|
+
if release_score == :green
|
28
|
+
puts ' - App has had recent updates.'
|
29
|
+
elsif release_score == :yellow
|
30
|
+
puts " - It's been a while since this app has released an update, but stable apps usually don't release more than once a year."
|
31
|
+
else
|
32
|
+
puts " - It's been over #{months_between(a[:current_version_release_date], Date.today)} months since this app has released. Thats pretty bad, and may be an indicator of a dead app (number of ratings may say otherwise)."
|
33
|
+
end
|
34
|
+
|
35
|
+
user_rating_count = a[:user_rating_count] || 0
|
36
|
+
|
37
|
+
if user_rating_count < 99
|
38
|
+
puts ' - App has very few ratings, which usually means very few downloads.'
|
39
|
+
elsif user_rating_count < 300
|
40
|
+
puts ' - App has a moderate number of reviews. If the app has been recently updated, then this is probably a new app.'
|
41
|
+
elsif user_rating_count > 10_000
|
42
|
+
puts " - App has an astronomical number of reviews (#{user_rating_count.commas}). If your app is similar to this one, you probably shouldn't build yours because you have little to no chance of \"beating them\"."
|
43
|
+
elsif user_rating_count > 5_000
|
44
|
+
puts " - App has a very high number of reviews (#{user_rating_count.commas} with an average rating of #{a[:average_user_rating]}). If you're app is similar to this one, you've got some serious competition. Success is unlikely."
|
45
|
+
else
|
46
|
+
puts " - App has a solid number of reviews (#{user_rating_count.commas} with an average rating of #{a[:average_user_rating]}). If you're app is similar to this one, you've got some competition, but you may be able to \"beat them\" if they have a low rating."
|
47
|
+
end
|
48
|
+
|
49
|
+
life_time_of_app = months_between(a[:release_date], Date.today)
|
50
|
+
|
51
|
+
if life_time_of_app.zero?
|
52
|
+
puts " - This app has been released recently. I can't project any revenue numbers because of this (try again in a month)."
|
53
|
+
elsif user_rating_count.zero?
|
54
|
+
puts ' - This app has no ratings. It either has a very poor review conversion rate, or (more likely) has never been downloaded.'
|
55
|
+
else
|
56
|
+
if a[:price].zero?
|
57
|
+
money_per_download = (1.99 * 0.7)
|
58
|
+
life_time_revenue_top_end = (((user_rating_count * 0.05) * 100) * money_per_download).round.to_i
|
59
|
+
industry = (((user_rating_count * 0.02) * 50) * money_per_download).round.to_i
|
60
|
+
life_time_revenue_bottom_end = (((user_rating_count * 0.005) * 20) * money_per_download).round.to_i
|
61
|
+
else
|
62
|
+
money_per_download = a[:price] * 0.7
|
63
|
+
life_time_revenue_top_end = ((user_rating_count * 100) * money_per_download).round.to_i
|
64
|
+
industry = ((user_rating_count * 50) * money_per_download).round.to_i
|
65
|
+
life_time_revenue_bottom_end = ((user_rating_count * 20) * money_per_download).round.to_i
|
66
|
+
end
|
67
|
+
|
68
|
+
monthly_revenue_top = life_time_revenue_top_end.fdiv(life_time_of_app).round.to_i
|
69
|
+
monthly_revenue_industry = industry.fdiv(life_time_of_app).round.to_i
|
70
|
+
monthly_revenue_bottom = life_time_revenue_bottom_end.fdiv(life_time_of_app).round.to_i
|
71
|
+
|
72
|
+
puts " - At best, this app has made $#{life_time_revenue_top_end.commas} over its lifetime (or $#{monthly_revenue_top.commas} a month)."
|
73
|
+
puts " - Based on my own industry measurements, this app probably made $#{industry.commas} over its lifetime (or $#{monthly_revenue_industry.commas} a month)."
|
74
|
+
puts " - Conservatively, this app has made $#{life_time_revenue_bottom_end.commas} over its lifetime (or $#{monthly_revenue_bottom.commas} a month)."
|
75
|
+
puts ''
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.determine_release_score app
|
81
|
+
months_since_last_release = months_between(app[:current_version_release_date], Date.today)
|
82
|
+
if months_since_last_release.zero?
|
83
|
+
:green
|
84
|
+
elsif months_since_last_release < 16
|
85
|
+
:yellow
|
86
|
+
else
|
87
|
+
:red
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.months_between start_date, end_date
|
92
|
+
(end_date.year * 12 + end_date.month) - (start_date.year * 12 + start_date.month)
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'pp'
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
class String
|
8
|
+
def underscore
|
9
|
+
self.gsub(/::/, '/').
|
10
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
11
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
12
|
+
tr("-", "_").
|
13
|
+
downcase
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class MotionSteward::AppStoreSearch
|
18
|
+
def self.search_for_app term
|
19
|
+
results = get 'search',
|
20
|
+
media: 'software',
|
21
|
+
term: URI.encode(term),
|
22
|
+
country: 'us',
|
23
|
+
limit: 15
|
24
|
+
|
25
|
+
pluck results['results'],
|
26
|
+
%w(genres price track_name track_id average_user_rating user_rating_count release_date current_version_release_date price)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.pluck json, values
|
30
|
+
if json.is_a? Array
|
31
|
+
json.map do |o|
|
32
|
+
to_hash(o).select { |k, _| values.include? k.to_s }
|
33
|
+
end
|
34
|
+
else
|
35
|
+
to_hash(json).select { |k, _| values.include? k }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.construct_uri(path)
|
40
|
+
URI.parse('https://itunes.apple.com/' + path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.convert_to_date_maybe k, v
|
44
|
+
return v if k !~ /date/i
|
45
|
+
|
46
|
+
begin
|
47
|
+
Date.parse(v).to_date
|
48
|
+
rescue
|
49
|
+
v
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.to_hash h
|
54
|
+
h.inject({}) do |memo, (k, v)|
|
55
|
+
new_v = convert_to_date_maybe(k, v)
|
56
|
+
memo[k.underscore.to_sym] = new_v
|
57
|
+
memo
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.get path, querystring
|
62
|
+
first = true
|
63
|
+
querystring.each do |k, v|
|
64
|
+
if first
|
65
|
+
path += '?'
|
66
|
+
first = false
|
67
|
+
else
|
68
|
+
path += '&'
|
69
|
+
end
|
70
|
+
path += "#{k}=#{v}"
|
71
|
+
end
|
72
|
+
|
73
|
+
uri = construct_uri path
|
74
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
75
|
+
http.use_ssl = true
|
76
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
77
|
+
response = http.request(req)
|
78
|
+
JSON.parse(response.body)
|
79
|
+
end
|
80
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: motion-steward
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Amir Rajan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-05-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: fastlane
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.32.1
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.32.1
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3.0'
|
33
|
+
description: There is a lot that goes into making a successful mobile app. This cli
|
34
|
+
tool will help you research possible app ideas and then scaffold a starting point
|
35
|
+
for a RubyMotion app.
|
36
|
+
email: ar@amirrajan.net
|
37
|
+
executables:
|
38
|
+
- motion-steward
|
39
|
+
extensions: []
|
40
|
+
extra_rdoc_files: []
|
41
|
+
files:
|
42
|
+
- bin/motion-steward
|
43
|
+
- lib/motion-steward.rb
|
44
|
+
- lib/motion-steward/app_store_research.rb
|
45
|
+
- lib/motion-steward/app_store_search.rb
|
46
|
+
homepage: http://rubymotion.com
|
47
|
+
licenses:
|
48
|
+
- MIT
|
49
|
+
metadata: {}
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
requirements: []
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 2.5.1
|
67
|
+
signing_key:
|
68
|
+
specification_version: 4
|
69
|
+
summary: CLI app that helps steward one through RubyMotion.
|
70
|
+
test_files: []
|