app-store-emigrant 0.0.1
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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE +23 -0
- data/README.md +5 -0
- data/Rakefile +8 -0
- data/app-store-emigrant.gemspec +29 -0
- data/bin/ase +5 -0
- data/lib/app-store-emigrant/app.rb +121 -0
- data/lib/app-store-emigrant/cli.rb +36 -0
- data/lib/app-store-emigrant/library.rb +89 -0
- data/lib/app-store-emigrant/version.rb +8 -0
- data/lib/app-store-emigrant.rb +11 -0
- data/specs/app.rb +51 -0
- data/specs/fixtures/dummy-library/Dummy.ipa +0 -0
- data/specs/fixtures/dummy-library/Invalid.extension +0 -0
- data/specs/fixtures/dummy-library/Outdated.ipa +0 -0
- data/specs/fixtures/dummy-library/iTunesMetadata.plist +10 -0
- data/specs/helpers.rb +3 -0
- data/specs/library.rb +32 -0
- metadata +111 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
Licensed under the MIT license
|
3
|
+
|
4
|
+
Copyright (c) 2012 Tim Kurvers <tim@moonsphere.net>
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
20
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
21
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
22
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
23
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
$:.push File.expand_path('../lib', __FILE__)
|
4
|
+
|
5
|
+
require 'app-store-emigrant/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = 'app-store-emigrant'
|
9
|
+
s.version = AppStore::Emigrant::VERSION
|
10
|
+
s.authors = ['Tim Kurvers']
|
11
|
+
s.email = ['tim@moonsphere.net']
|
12
|
+
s.homepage = 'http://moonsphere.net'
|
13
|
+
s.summary = 'App Store Emigrant will manually attempt to verify whether any of your local mobile applications are out of date'
|
14
|
+
s.description = 'Moved countries and now iTunes refuses to notify you properly with updates? Rest assured, using App Store Emigrant you know exactly which apps are outdated'
|
15
|
+
|
16
|
+
s.rubyforge_project = 'app-store-emigrant'
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ['lib']
|
22
|
+
|
23
|
+
s.add_dependency 'rubyzip', '~> 0.9.5'
|
24
|
+
s.add_dependency 'rainbow', '~> 1.1.3'
|
25
|
+
s.add_dependency 'CFPropertyList', '~> 2.0.17'
|
26
|
+
|
27
|
+
s.add_development_dependency 'rake'
|
28
|
+
|
29
|
+
end
|
data/bin/ase
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'cfpropertylist'
|
2
|
+
require 'json'
|
3
|
+
require 'net/http'
|
4
|
+
require 'pathname'
|
5
|
+
require 'zip/zip'
|
6
|
+
require 'zip/zipfilesystem'
|
7
|
+
|
8
|
+
module AppStore; end
|
9
|
+
|
10
|
+
module AppStore::Emigrant
|
11
|
+
|
12
|
+
# Represents a single iTunes mobile application
|
13
|
+
class App
|
14
|
+
|
15
|
+
# List of valid extensions for an application
|
16
|
+
VALID_EXTENSIONS = [
|
17
|
+
'.ipa'
|
18
|
+
]
|
19
|
+
|
20
|
+
# Regular expression to match a version number in filenames
|
21
|
+
FILENAME_VERSION_REGEX = Regexp.new('(?<version>[0-9.]+)(?:' + VALID_EXTENSIONS.join('|') + ')')
|
22
|
+
|
23
|
+
attr_reader :path
|
24
|
+
|
25
|
+
# Initializes application from given path
|
26
|
+
def initialize path
|
27
|
+
@path = Pathname.new path
|
28
|
+
@path = @path.expand_path unless @path.absolute?
|
29
|
+
|
30
|
+
# Ensure application exists
|
31
|
+
unless @path.file?
|
32
|
+
raise DoesNotExist, "Given path is not a valid application: #{@path}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Ensure application has valid extension
|
36
|
+
unless VALID_EXTENSIONS.include? @path.extname
|
37
|
+
raise Invalid, "Given application does not have a valid extension: #{@path}"
|
38
|
+
end
|
39
|
+
|
40
|
+
@metadata = nil
|
41
|
+
@clouddata = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# Filename of this application (including extension)
|
45
|
+
def filename
|
46
|
+
unless @filename
|
47
|
+
@filename = @path.basename().to_s
|
48
|
+
end
|
49
|
+
|
50
|
+
@filename
|
51
|
+
end
|
52
|
+
|
53
|
+
# Name of this application (from its metadata)
|
54
|
+
def name
|
55
|
+
metadata['itemName']
|
56
|
+
end
|
57
|
+
|
58
|
+
# Version of this application
|
59
|
+
def version
|
60
|
+
unless @version
|
61
|
+
|
62
|
+
# Extract version information (if available)
|
63
|
+
@version = metadata['bundleShortVersionString'] || metadata['bundleVersion']
|
64
|
+
|
65
|
+
# Otherwise, use the filename
|
66
|
+
unless @version
|
67
|
+
@version = filename[FILENAME_VERSION_REGEX, :version]
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
@version
|
73
|
+
end
|
74
|
+
|
75
|
+
# Lazily loads local metadata for this application from its iTunesMetadata.plist
|
76
|
+
def metadata
|
77
|
+
unless @metadata
|
78
|
+
begin
|
79
|
+
Zip::ZipFile.open(@path) do |zip|
|
80
|
+
data = zip.file.read('iTunesMetadata.plist')
|
81
|
+
plist = CFPropertyList::List.new(data: data)
|
82
|
+
@metadata = CFPropertyList.native_types(plist.value)
|
83
|
+
end
|
84
|
+
rescue Zip::ZipError => e
|
85
|
+
raise Invalid, e.message
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
@metadata
|
90
|
+
end
|
91
|
+
|
92
|
+
# Lazily queries Apple's iTunes Store API for latest cloud data
|
93
|
+
# Note: Clouddata may be nil if the application was removed from the store
|
94
|
+
def clouddata
|
95
|
+
unless @clouddata
|
96
|
+
response = Net::HTTP.get('itunes.apple.com', '/lookup?id=' + metadata['itemId'].to_s)
|
97
|
+
@clouddata = JSON.parse(response)['results'].first
|
98
|
+
end
|
99
|
+
|
100
|
+
@clouddata
|
101
|
+
end
|
102
|
+
|
103
|
+
# Version available in the cloud
|
104
|
+
def cloudversion
|
105
|
+
clouddata && clouddata['version']
|
106
|
+
end
|
107
|
+
|
108
|
+
# Whether this application is outdated
|
109
|
+
def outdated?
|
110
|
+
return cloudversion && version != cloudversion
|
111
|
+
end
|
112
|
+
|
113
|
+
# Raised when an application does not exist
|
114
|
+
class DoesNotExist < StandardError; end
|
115
|
+
|
116
|
+
# Raised when an application is invalid (e.g non-valid extension)
|
117
|
+
class Invalid < StandardError; end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rainbow'
|
2
|
+
|
3
|
+
module AppStore; end
|
4
|
+
|
5
|
+
module AppStore::Emigrant
|
6
|
+
|
7
|
+
# Represents the command line interface
|
8
|
+
class CLI
|
9
|
+
|
10
|
+
# Initializes CLI instance with given arguments
|
11
|
+
def initialize args
|
12
|
+
|
13
|
+
# Extract the path (if any)
|
14
|
+
path, = args
|
15
|
+
|
16
|
+
# Use the default library on this system when no path is specified
|
17
|
+
library = path ? Library.new(path) : Library.default
|
18
|
+
|
19
|
+
# Loop through all applications
|
20
|
+
library.apps.each do |app|
|
21
|
+
|
22
|
+
# And print their name, version and whether it's outdated
|
23
|
+
print app.name + ' @ ' + "v#{app.version}".foreground(app.outdated? ? :red : :green)
|
24
|
+
if app.outdated?
|
25
|
+
print " (v#{app.cloudversion} available in the cloud)"
|
26
|
+
end
|
27
|
+
puts
|
28
|
+
|
29
|
+
sleep(1)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module AppStore; end
|
4
|
+
|
5
|
+
module AppStore::Emigrant
|
6
|
+
|
7
|
+
# Represents a single iTunes mobile applications library
|
8
|
+
class Library
|
9
|
+
|
10
|
+
attr_reader :path
|
11
|
+
|
12
|
+
# Initializes library from given path
|
13
|
+
def initialize path
|
14
|
+
@path = Pathname.new path
|
15
|
+
@path = @path.expand_path unless @path.absolute?
|
16
|
+
|
17
|
+
# Ensure library is a valid directory
|
18
|
+
unless @path.directory?
|
19
|
+
raise DoesNotExist, "Given path is not a valid library directory: #{@path}"
|
20
|
+
end
|
21
|
+
|
22
|
+
@apps = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# Retrieves a list of applications
|
26
|
+
def apps
|
27
|
+
unless @apps
|
28
|
+
load!
|
29
|
+
end
|
30
|
+
|
31
|
+
@apps
|
32
|
+
end
|
33
|
+
|
34
|
+
# Forcefully loads applications from disk
|
35
|
+
def load!
|
36
|
+
@apps = []
|
37
|
+
@path.each_child do |item|
|
38
|
+
if item.file?
|
39
|
+
begin
|
40
|
+
@apps << App.new(item)
|
41
|
+
rescue App::Invalid; end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
# Searches for applications containing given snippet in filename
|
48
|
+
def find snippet
|
49
|
+
apps.select do |app|
|
50
|
+
app.filename.include? snippet
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the default library (if any) for this system
|
55
|
+
# See Apple's support document (http://support.apple.com/kb/ht1391) as to where libraries can be found
|
56
|
+
def self.default
|
57
|
+
|
58
|
+
# Use the homedir provided through the environment
|
59
|
+
homedir = ENV['HOME']
|
60
|
+
|
61
|
+
# List all locations
|
62
|
+
locations = [
|
63
|
+
|
64
|
+
# Mac OSX and Windows Vista
|
65
|
+
"#{homedir}/Music/iTunes/iTunes Media/Mobile Applications/",
|
66
|
+
|
67
|
+
# Windows 7
|
68
|
+
"#{homedir}/My Music/iTunes/iTunes Media/Mobile Applications/",
|
69
|
+
|
70
|
+
# Windows XP
|
71
|
+
"#{homedir}/My Documents/My Music/iTunes/iTunes Media/Mobile Applications/"
|
72
|
+
]
|
73
|
+
|
74
|
+
# Raise exception if no default library could be found
|
75
|
+
path = Dir.glob(locations).first
|
76
|
+
unless path
|
77
|
+
raise DoesNotExist, 'Could not locate default iTunes library'
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return an instance of this default library
|
81
|
+
Library.new path
|
82
|
+
end
|
83
|
+
|
84
|
+
# Raised when a library does not exist
|
85
|
+
class DoesNotExist < StandardError; end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
data/specs/app.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'app-store-emigrant'
|
2
|
+
require 'helpers'
|
3
|
+
require 'minitest/spec'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
|
6
|
+
include AppStore::Emigrant
|
7
|
+
|
8
|
+
describe App do
|
9
|
+
|
10
|
+
LIBRARY = ROOT + '/fixtures/dummy-library'
|
11
|
+
|
12
|
+
before do
|
13
|
+
@dummy = App.new(LIBRARY + '/Dummy.ipa')
|
14
|
+
@outdated = App.new(LIBRARY + '/Outdated.ipa')
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'must be a valid file on disk' do
|
18
|
+
lambda do
|
19
|
+
App.new(LIBRARY + '/Non-Existent.ipa')
|
20
|
+
end.must_raise App::DoesNotExist
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'must have a valid extension' do
|
24
|
+
lambda do
|
25
|
+
App.new(LIBRARY + '/Non-Existent.ipa')
|
26
|
+
end.must_raise App::DoesNotExist
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'can determine its own filename' do
|
30
|
+
@dummy.filename.must_equal 'Dummy.ipa'
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'can handle invalid structures' do
|
34
|
+
lambda do
|
35
|
+
@dummy.version
|
36
|
+
end.must_raise App::Invalid
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'can query local metadata' do
|
40
|
+
@outdated.version.must_equal '0.9'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'can query clouddata' do
|
44
|
+
@outdated.cloudversion.must_equal '1.1.0'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'can determine whether it is outdated' do
|
48
|
+
@outdated.outdated?.must_equal true
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
File without changes
|
File without changes
|
Binary file
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
3
|
+
<plist version="1.0">
|
4
|
+
<dict>
|
5
|
+
<key>bundleVersion</key>
|
6
|
+
<string>0.9</string>
|
7
|
+
<key>itemId</key>
|
8
|
+
<integer>344186162</integer>
|
9
|
+
</dict>
|
10
|
+
</plist>
|
data/specs/helpers.rb
ADDED
data/specs/library.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'app-store-emigrant'
|
2
|
+
require 'helpers'
|
3
|
+
require 'minitest/spec'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
|
6
|
+
include AppStore::Emigrant
|
7
|
+
|
8
|
+
describe Library do
|
9
|
+
|
10
|
+
before do
|
11
|
+
@library = Library.new(ROOT + '/fixtures/dummy-library')
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'must be located in a valid directory' do
|
15
|
+
@library.must_be_instance_of Library
|
16
|
+
|
17
|
+
lambda do
|
18
|
+
Library.new(ROOT + '/non-existent-library')
|
19
|
+
end.must_raise Library::DoesNotExist
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'will gracefully and lazily load applications' do
|
23
|
+
@library.instance_variable_get('@apps').must_be_nil
|
24
|
+
@library.apps.must_be_instance_of Array
|
25
|
+
@library.apps.length.must_equal 2
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'can find applications' do
|
29
|
+
@library.find('Dummy').length.must_equal 1
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: app-store-emigrant
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tim Kurvers
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-27 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rubyzip
|
16
|
+
requirement: &70227983767300 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.9.5
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70227983767300
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rainbow
|
27
|
+
requirement: &70227984172040 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.1.3
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70227984172040
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: CFPropertyList
|
38
|
+
requirement: &70227984171580 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.0.17
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70227984171580
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &70227984171200 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70227984171200
|
58
|
+
description: Moved countries and now iTunes refuses to notify you properly with updates?
|
59
|
+
Rest assured, using App Store Emigrant you know exactly which apps are outdated
|
60
|
+
email:
|
61
|
+
- tim@moonsphere.net
|
62
|
+
executables:
|
63
|
+
- ase
|
64
|
+
extensions: []
|
65
|
+
extra_rdoc_files: []
|
66
|
+
files:
|
67
|
+
- .gitignore
|
68
|
+
- Gemfile
|
69
|
+
- LICENSE
|
70
|
+
- README.md
|
71
|
+
- Rakefile
|
72
|
+
- app-store-emigrant.gemspec
|
73
|
+
- bin/ase
|
74
|
+
- lib/app-store-emigrant.rb
|
75
|
+
- lib/app-store-emigrant/app.rb
|
76
|
+
- lib/app-store-emigrant/cli.rb
|
77
|
+
- lib/app-store-emigrant/library.rb
|
78
|
+
- lib/app-store-emigrant/version.rb
|
79
|
+
- specs/app.rb
|
80
|
+
- specs/fixtures/dummy-library/Dummy.ipa
|
81
|
+
- specs/fixtures/dummy-library/Invalid.extension
|
82
|
+
- specs/fixtures/dummy-library/Outdated.ipa
|
83
|
+
- specs/fixtures/dummy-library/iTunesMetadata.plist
|
84
|
+
- specs/helpers.rb
|
85
|
+
- specs/library.rb
|
86
|
+
homepage: http://moonsphere.net
|
87
|
+
licenses: []
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project: app-store-emigrant
|
106
|
+
rubygems_version: 1.8.15
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: App Store Emigrant will manually attempt to verify whether any of your local
|
110
|
+
mobile applications are out of date
|
111
|
+
test_files: []
|