skyjam 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b72293adf1cb2d6ca7d3e9304e692a4e7d7b5f78
4
+ data.tar.gz: b9fccfea53692433410e312b21e4a40089bd55f9
5
+ SHA512:
6
+ metadata.gz: 2f96ff58ba7b7248818fb5771f5998083e9216c512a1db101ce08a4798d2d868e1d050e81cf66d7871acde34a1d53b44141efdc8b876f81dadb1cd25b2227d3d
7
+ data.tar.gz: 7b20cb56b5e4f7cfc8726f4a4f338002867c4da7a10e907697cc5b74e6f674d2fda2708f177bcc3549489a5934aa28d002ee2b16050aadcdb558139905b40b7f
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (C) 2013 Loic Nageleisen
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name of the copyright holders nor the
12
+ names of its contributors may be used to endorse or promote products
13
+ derived from this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # ruby-skyjam
2
+
3
+ Deftly interact with Google Music (a.k.a Skyjam)
4
+
5
+ ## Important foreword
6
+
7
+ This uses the same *device id* as *Google's Music Manager* (your MAC address).
8
+ The reason is that incrementing the MAC is not a globally solving solution
9
+ and that you most probably won't need Google's Music Manager afterwards
10
+ because it's, well, “lacking” (to say the least). This may apply to the
11
+ *Chrome extension* too but I'm not sure so you'd be best not using it (it's
12
+ similarly lacking anyway).
13
+
14
+ ## On the command line
15
+
16
+ ```bash
17
+ gem install skyjam # install the gem
18
+ skyjam --auth # authenticates with OAuth, do it only once
19
+ skyjam ~/Music/Skyjam # download files into the specified directory
20
+ ```
21
+
22
+ Existing files will not be overwritten, so that makes
23
+ a nice sync/resume solution. Tracks are downloaded atomically, so
24
+ you're safe to `^C`.
25
+
26
+ ## Inside ruby
27
+
28
+ Add 'skyjam' to your `Gemfile` (you *do* use Gemfiles?)
29
+
30
+ Here's a sample of what you can do now:
31
+
32
+ ```ruby
33
+ require 'skyjam'
34
+
35
+ # where you want to store your library
36
+ path = File.join(ENV['HOME'], 'Music/Skyjam')
37
+
38
+ # Interactive authentication, only needed once.
39
+ # This performs OAuth and registers the device
40
+ # into Google Music services. Tokens are persisted
41
+ # in ~/.config/skyjam
42
+ Skyjam::Library.auth
43
+
44
+ # Connect the library to Google Music services
45
+ # This performs an OAuth refresh with persisted
46
+ # tokens
47
+ lib = Skyjam::Library.connect(path)
48
+
49
+ puts lib.tracks.count
50
+
51
+ lib.tracks.take(5).each { |t| puts t.title } # metadata is exposed as accessors
52
+
53
+ track = lib.tracks.first
54
+ track.download # atomically download track into the library
55
+ track.download # noop, since now the file exists
56
+ track.download(lazy: false) # forces the download
57
+
58
+ track.data # returns track audio data from the file (since it's downloaded)
59
+ track.data(remote: true) # forces remote data fetching
60
+ ```
61
+
62
+ The following snippet also makes for an interesting interactive
63
+ REPL to interact with Google Music, wich is a testament to the
64
+ clarity aimed at in this project:
65
+
66
+ ```ruby
67
+ require 'skyjam'
68
+ require 'pry'
69
+ Skyjam::Library.connect('some/where').pry
70
+ ```
71
+
72
+ ## Future features
73
+
74
+ Yes, trouble free upload for the quality minded is coming.
75
+
76
+ Also, see [TODO](TODO.md).
77
+
78
+ ## Goals
79
+
80
+ Have a potent tool that doubles as a library and a documentation
81
+ of the Google Music API (including ProtoBuf definitions)
82
+
83
+ ## References
84
+
85
+ * [Simon Weber's Unofficial Google Music API](https://github.com/simon-weber/Unofficial-Google-Music-API/)
86
+ * [Google Music protocol reverse engineering effort](http://www.verious.com/code/antimatter15/google-music-protocol/) (disappeared)
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ load 'protobuf/tasks/compile.rake'
2
+
3
+ task :compile do
4
+ args = %w(skyjam defs lib ruby .pb.rb)
5
+ ::Rake::Task['protobuf:compile'].invoke(*args)
6
+ end
data/bin/skyjam ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'skyjam'
4
+ require 'rainbow'
5
+ require 'rainbow/ext/string'
6
+
7
+ if ARGV.size != 1
8
+ $stderr.puts('usage: skyjam [--auth | library_path]')
9
+ exit(1)
10
+ end
11
+
12
+ module SkyJam
13
+ path = ARGV.last
14
+
15
+ if ARGV[0] == '--auth'
16
+ path = nil if path == '--auth'
17
+ rc = Library.auth(path) ? 0 : 1
18
+ exit(rc)
19
+ end
20
+
21
+ begin
22
+ lib = Library.connect(path)
23
+ rescue Client::Error => e
24
+ $stderr.puts("error: #{e.message}")
25
+ exit(1)
26
+ end
27
+
28
+ begin
29
+ success = failed = 0
30
+
31
+ lib.tracks.each.with_index do |track, i|
32
+ begin
33
+ $stdout.write(" %05d #{track.title}" % i)
34
+ $stdout.flush
35
+ track.download(lazy: true)
36
+ rescue Client::Error
37
+ $stdout.write("\r" + 'NOK'.color(:red))
38
+ failed += 1
39
+ else
40
+ $stdout.write("\r" + ' OK'.color(:green))
41
+ success += 1
42
+ end
43
+ $stdout.write("\n")
44
+ end
45
+ ensure
46
+ $stdout.write("\n")
47
+ $stdout.write("summary: success %s | failed: %s\n" %
48
+ [success.to_s.color(:green),
49
+ failed.to_s.color(:red)])
50
+ end
51
+ end
@@ -0,0 +1,289 @@
1
+ module SkyJam
2
+ class Client
3
+ class Error < StandardError; end
4
+
5
+ def initialize
6
+ @base_url = 'https://play.google.com/music/'
7
+ @service_url = @base_url + 'services/'
8
+ @android_url = 'https://android.clients.google.com/upsj/'
9
+
10
+ load_config
11
+ end
12
+
13
+ ### Simple auth
14
+ # login: with app-specific password, obtain auth token
15
+ # cookie: with auth token, obtain cross token (xt)
16
+ # loadalltracks: with auth token and cross token, obtain list of tracks
17
+
18
+ def login
19
+ uri = URI('https://www.google.com/accounts/ClientLogin')
20
+
21
+ q = { 'service' => 'sj',
22
+ 'account_type' => 'GOOGLE',
23
+ 'source' => 'ruby-skyjam-%s' % SkyJam::VERSION,
24
+ 'Email' => @account,
25
+ 'Passwd' => @password }
26
+
27
+ http = Net::HTTP.new(uri.host, uri.port)
28
+ http.use_ssl = true
29
+
30
+ req = Net::HTTP::Post.new(uri.path)
31
+ req.set_form_data(q)
32
+ res = http.request(req)
33
+
34
+ unless res.is_a? Net::HTTPSuccess
35
+ fail Error, 'login failed: #{res.code}'
36
+ end
37
+
38
+ tokens = Hash[*res.body
39
+ .split("\n")
40
+ .map { |r| r.split('=', 2) }
41
+ .flatten]
42
+ @sid = tokens['SID']
43
+ @auth = tokens['Auth']
44
+ end
45
+
46
+ def cookie
47
+ uri = URI(@base_url + 'listen')
48
+
49
+ http = Net::HTTP.new(uri.host, uri.port)
50
+ http.use_ssl = true
51
+
52
+ req = Net::HTTP::Head.new(uri.path)
53
+ req['Authorization'] = 'GoogleLogin auth=%s' % @auth
54
+ res = http.request(req)
55
+
56
+ unless res.is_a? Net::HTTPSuccess
57
+ fail Error, 'cookie failed: #{res.code}'
58
+ end
59
+
60
+ h = res.to_hash['set-cookie']
61
+ .map { |e| e =~ /^xt=([^;]+);/ and $1 }
62
+ .compact.first
63
+ @cookie = h
64
+ end
65
+
66
+ ## Web Client API
67
+
68
+ def loadalltracks
69
+ uri = URI(@service_url + 'loadalltracks')
70
+
71
+ http = Net::HTTP.new(uri.host, uri.port)
72
+ http.use_ssl = true
73
+
74
+ req = Net::HTTP::Post.new(uri.path)
75
+ req.set_form_data('u' => 0, 'xt' => @cookie)
76
+ req['Authorization'] = 'GoogleLogin auth=%s' % @auth
77
+ res = http.request(req)
78
+
79
+ unless res.is_a? Net::HTTPSuccess
80
+ fail Error, 'loadalltracks failed: #{res.code}'
81
+ end
82
+
83
+ JSON.parse(res.body)
84
+ end
85
+
86
+ ## OAuth2
87
+ # https://developers.google.com/accounts/docs/OAuth2InstalledApp
88
+ # https://code.google.com/apis/console
89
+
90
+ def oauth2_access
91
+ { client_id: '256941431767.apps.googleusercontent.com',
92
+ client_secret: 'oHTZP8zhh7E8wF6NWsiDULhq' }
93
+ end
94
+
95
+ def oauth2_endpoint
96
+ { site: 'https://accounts.google.com',
97
+ authorize_url: '/o/oauth2/auth',
98
+ token_url: '/o/oauth2/token' }
99
+ end
100
+
101
+ def oauth2_request
102
+ { scope: 'https://www.googleapis.com/auth/musicmanager',
103
+ access_type: 'offline',
104
+ approval_prompt: 'force',
105
+ redirect_uri: 'urn:ietf:wg:oauth:2.0:oob' }
106
+ end
107
+
108
+ def oauth2_client
109
+ @oauth2_client ||= OAuth2::Client.new(oauth2_access[:client_id],
110
+ oauth2_access[:client_secret],
111
+ oauth2_endpoint)
112
+ @oauth2_client
113
+ end
114
+
115
+ def oauth2_setup
116
+ # ask for OOB auth code
117
+ puts oauth2_client.auth_code.authorize_url(oauth2_request)
118
+ # user gives code
119
+ puts 'code: '
120
+ code = $stdin.gets.chomp.strip
121
+ # exchange code for access token and refresh token
122
+ uri = oauth2_request[:redirect_uri]
123
+ access = oauth2_client.auth_code.get_token(code,
124
+ redirect_uri: uri,
125
+ token_method: :post)
126
+ puts 'access: ' + access.token
127
+ puts 'refresh: ' + access.refresh_token
128
+ # expires_in
129
+ # token_type: Bearer
130
+ @oauth2_access_token = access
131
+ end
132
+
133
+ def oauth2_persist(filename)
134
+ File.open(filename, 'wb') do |f|
135
+ f.write(YAML.dump(refresh_token: @oauth2_access_token.refresh_token))
136
+ end
137
+ end
138
+
139
+ def oauth2_restore(filename)
140
+ token_h = YAML.load(File.read(filename))
141
+ oauth2_login(token_h[:refresh_token])
142
+ end
143
+
144
+ def oauth2_login(refresh_token)
145
+ @oauth2_access_token = OAuth2::AccessToken
146
+ .from_hash(oauth2_client,
147
+ refresh_token: refresh_token)
148
+ oauth2_refresh_access_token
149
+ end
150
+
151
+ def oauth2_refresh_access_token
152
+ @oauth2_access_token = @oauth2_access_token.refresh!
153
+ end
154
+
155
+ def oauth2_access_token_expired?
156
+ @oauth2_access_token.expired?
157
+ end
158
+
159
+ def oauth2_authentication_header
160
+ @oauth2_access_token.options[:header_format] % @oauth2_access_token.token
161
+ end
162
+
163
+ ## MusicManager Uploader identification
164
+
165
+ def mac_addr
166
+ case RUBY_PLATFORM
167
+ when /darwin/
168
+ if (m = `ifconfig en0`.match(/ether (\S{17})/))
169
+ m[1].upcase
170
+ end
171
+ end
172
+ end
173
+
174
+ def hostname
175
+ `hostname`.chomp.gsub(/\.local$/, '')
176
+ end
177
+
178
+ def uploader_id
179
+ mac_addr.gsub(/\d{2}$/) { |s| '%02X' % s.hex }
180
+ end
181
+
182
+ def uploader_name
183
+ "#{hostname} (ruby-skyjam-#{VERSION})"
184
+ end
185
+
186
+ def uploader_auth
187
+ # {'User-agent': 'Music Manager (1, 0, 55, 7425 HTTPS - Windows)'}'
188
+ # uploader_id uploader_name
189
+ pb_body = MusicManager::AuthRequest.new
190
+ pb_body.id = uploader_id
191
+ pb_body.name = uploader_name
192
+
193
+ uri = URI(@android_url + 'upauth')
194
+
195
+ http = Net::HTTP.new(uri.host, uri.port)
196
+ http.use_ssl = true
197
+
198
+ req = Net::HTTP::Post.new(uri.path)
199
+ req.body = pb_body.serialize_to_string
200
+ req['Content-Type'] = 'application/x-google-protobuf'
201
+ req['Authorization'] = oauth2_authentication_header
202
+ res = http.request(req)
203
+
204
+ unless res.is_a? Net::HTTPSuccess
205
+ fail Error, 'uploader_auth failed: #{res.code}'
206
+ end
207
+
208
+ MusicManager::Response.new.parse_from_string(res.body)
209
+ end
210
+
211
+ ## MusicManager API
212
+
213
+ def listtracks(continuation_token: nil)
214
+ oauth2_refresh_access_token if oauth2_access_token_expired?
215
+
216
+ pb_body = MusicManager::ExportTracksRequest.new
217
+ pb_body.client_id = uploader_id
218
+ pb_body.export_type = MusicManager::ExportTracksRequest::TrackType::ALL
219
+ pb_body.continuation_token = continuation_token unless continuation_token.nil?
220
+
221
+ uri = URI('https://music.google.com/music/exportids')
222
+
223
+ http = Net::HTTP.new(uri.host, uri.port)
224
+ http.use_ssl = true
225
+
226
+ req = Net::HTTP::Post.new(uri.path)
227
+ req.body = pb_body.serialize_to_string
228
+ req['Content-Type'] = 'application/x-google-protobuf'
229
+ req['Authorization'] = oauth2_authentication_header
230
+ req['X-Device-ID'] = uploader_id
231
+ res = http.request(req)
232
+
233
+ unless res.is_a? Net::HTTPSuccess
234
+ fail Error, 'listtracks failed: #{res.code}'
235
+ end
236
+
237
+ MusicManager::ExportTracksResponse.new.parse_from_string(res.body)
238
+ end
239
+
240
+ def download_url(song_id)
241
+ uri = URI('https://music.google.com/music/export')
242
+ q = { version: 2, songid: song_id }
243
+
244
+ http = Net::HTTP.new(uri.host, uri.port)
245
+ http.use_ssl = true
246
+
247
+ qs = q.map { |k, v| "#{k}=#{v}" }.join('&')
248
+ req = Net::HTTP::Get.new(uri.path + '?' + qs)
249
+ req['Authorization'] = oauth2_authentication_header
250
+ req['X-Device-ID'] = uploader_id
251
+ res = http.request(req)
252
+
253
+ unless res.is_a? Net::HTTPSuccess
254
+ fail Error, 'download_url failed: #{res.code}'
255
+ end
256
+
257
+ JSON.parse(res.body)['url']
258
+ end
259
+
260
+ def download_track(url)
261
+ uri = URI(url)
262
+
263
+ http = Net::HTTP.new(uri.host, uri.port)
264
+ http.use_ssl = true
265
+
266
+ req = Net::HTTP::Get.new(uri.path + '?' + uri.query)
267
+ req['Authorization'] = oauth2_authentication_header
268
+ #req['User-Agent'] = 'Music Manager (1, 0, 55, 7425 HTTPS - Windows)'
269
+ req['X-Device-ID'] = uploader_id
270
+ res = http.request(req)
271
+
272
+ unless res.is_a? Net::HTTPSuccess
273
+ fail Error, 'download_track failed: #{res.code}'
274
+ end
275
+
276
+ res.body
277
+ end
278
+
279
+ def read_config
280
+ YAML.load(File.read('auth.yml'))
281
+ end
282
+
283
+ def load_config
284
+ config = read_config
285
+ @account = config['account']
286
+ @password = config['password']
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,77 @@
1
+ module SkyJam
2
+ class Library
3
+ class << self
4
+ def auth(path = nil)
5
+ config = auth_config(path) || default_auth_config
6
+
7
+ client = Client.new
8
+ client.oauth2_setup
9
+ client.uploader_auth
10
+
11
+ FileUtils.mkdir_p(File.dirname(config))
12
+ client.oauth2_persist(config)
13
+ end
14
+
15
+ def connect(path)
16
+ library = new(path)
17
+
18
+ config = default_auth_config if File.exist?(default_auth_config)
19
+ config = auth_config(path) if File.exist?(auth_config(path))
20
+
21
+ fail Client::Error, 'no auth' if config.nil?
22
+
23
+ library.instance_eval do
24
+ @client = Client.new
25
+ @client.oauth2_restore(config)
26
+ end
27
+
28
+ library
29
+ end
30
+
31
+ private
32
+
33
+ def default_auth_config
34
+ File.join(ENV['HOME'], '.config/skyjam/skyjam.auth.yml')
35
+ end
36
+
37
+ def auth_config(path)
38
+ File.join(path, '.skyjam.auth.yml') unless path.nil?
39
+ end
40
+ end
41
+
42
+ attr_reader :path
43
+
44
+ def initialize(path)
45
+ @path = path
46
+ end
47
+
48
+ def tracks
49
+ return @tracks unless @tracks.nil?
50
+
51
+ @tracks = []
52
+ continuation_token = nil
53
+
54
+ loop do
55
+ list = client.listtracks(continuation_token: continuation_token)
56
+
57
+ continuation_token = list[:continuation_token]
58
+
59
+ list[:track_info].each do |info|
60
+ track = SkyJam::Track.new(info)
61
+ library = self
62
+ track.instance_eval { @library = library }
63
+
64
+ @tracks << track
65
+ end
66
+
67
+ break if continuation_token == ''
68
+ end
69
+
70
+ @tracks
71
+ end
72
+
73
+ private
74
+
75
+ attr_reader :client
76
+ end
77
+ end
@@ -0,0 +1,26 @@
1
+ ##
2
+ # This file is auto-generated. DO NOT EDIT!
3
+ #
4
+ require 'protobuf/message'
5
+
6
+ module SkyJam
7
+ module MusicManager
8
+
9
+ ##
10
+ # Message Classes
11
+ #
12
+ class AuthRequest < ::Protobuf::Message; end
13
+
14
+
15
+ ##
16
+ # Message Fields
17
+ #
18
+ class AuthRequest
19
+ required :string, :id, 1
20
+ optional :string, :name, 2
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
@@ -0,0 +1,35 @@
1
+ ##
2
+ # This file is auto-generated. DO NOT EDIT!
3
+ #
4
+ require 'protobuf/message'
5
+
6
+ module SkyJam
7
+ module MusicManager
8
+
9
+ ##
10
+ # Message Classes
11
+ #
12
+ class ExportTracksRequest < ::Protobuf::Message
13
+ class TrackType < ::Protobuf::Enum
14
+ define :ALL, 1
15
+ define :STORE, 2
16
+ end
17
+
18
+ end
19
+
20
+
21
+
22
+ ##
23
+ # Message Fields
24
+ #
25
+ class ExportTracksRequest
26
+ required :string, :client_id, 2
27
+ optional :string, :continuation_token, 3
28
+ optional ::SkyJam::MusicManager::ExportTracksRequest::TrackType, :export_type, 4
29
+ optional :int64, :updated_min, 5
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+
@@ -0,0 +1,50 @@
1
+ ##
2
+ # This file is auto-generated. DO NOT EDIT!
3
+ #
4
+ require 'protobuf/message'
5
+
6
+ module SkyJam
7
+ module MusicManager
8
+
9
+ ##
10
+ # Message Classes
11
+ #
12
+ class ExportTracksResponse < ::Protobuf::Message
13
+ class Status < ::Protobuf::Enum
14
+ define :OK, 1
15
+ define :TRANSIENT_ERROR, 2
16
+ define :MAX_CLIENTS, 3
17
+ define :CLIENT_AUTH_ERROR, 4
18
+ define :CLIENT_REG_ERROR, 5
19
+ end
20
+
21
+ class TrackInfo < ::Protobuf::Message; end
22
+
23
+ end
24
+
25
+
26
+
27
+ ##
28
+ # Message Fields
29
+ #
30
+ class ExportTracksResponse
31
+ class TrackInfo
32
+ optional :string, :id, 1
33
+ optional :string, :title, 2
34
+ optional :string, :album, 3
35
+ optional :string, :album_artist, 4
36
+ optional :string, :artist, 5
37
+ optional :int32, :track_number, 6
38
+ optional :int64, :track_size, 7
39
+ end
40
+
41
+ required ::SkyJam::MusicManager::ExportTracksResponse::Status, :status, 1
42
+ repeated ::SkyJam::MusicManager::ExportTracksResponse::TrackInfo, :track_info, 2
43
+ optional :string, :continuation_token, 3
44
+ optional :int64, :updated_min, 4
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
@@ -0,0 +1,67 @@
1
+ ##
2
+ # This file is auto-generated. DO NOT EDIT!
3
+ #
4
+ require 'protobuf/message'
5
+
6
+ module SkyJam
7
+ module MusicManager
8
+
9
+ ##
10
+ # Message Classes
11
+ #
12
+ class Response < ::Protobuf::Message
13
+ class Type < ::Protobuf::Enum
14
+ define :METADATA, 1
15
+ define :PLAYLIST, 2
16
+ define :PLAYLIST_ENTRY, 3
17
+ define :SAMPLE, 4
18
+ define :JOBS, 5
19
+ define :AUTH, 6
20
+ define :CLIENT_STATE, 7
21
+ define :UPDATE_UPLOAD_STATE, 8
22
+ define :DELETE_UPLOAD_REQUESTED, 9
23
+ end
24
+
25
+ class AuthStatus < ::Protobuf::Enum
26
+ define :OK, 8
27
+ define :MAX_LIMIT_REACHED, 9
28
+ define :CLIENT_BOUND_TO_OTHER_ACCOUNT, 10
29
+ define :CLIENT_NOT_AUTHORIZED, 11
30
+ define :MAX_PER_MACHINE_USERS_EXCEEDED, 12
31
+ define :CLIENT_PLEASE_RETRY, 13
32
+ define :NOT_SUBSCRIBED, 14
33
+ define :INVALID_REQUEST, 15
34
+ end
35
+
36
+ class Status < ::Protobuf::Message
37
+ class Code < ::Protobuf::Enum
38
+ define :OK, 1
39
+ define :ALREADY_EXISTS, 2
40
+ define :SOFT_ERROR, 3
41
+ define :METADATA_TOO_LARGE, 4
42
+ end
43
+
44
+ end
45
+
46
+
47
+ end
48
+
49
+
50
+
51
+ ##
52
+ # Message Fields
53
+ #
54
+ class Response
55
+ class Status
56
+ required ::SkyJam::MusicManager::Response::Status::Code, :code, 1
57
+ end
58
+
59
+ optional ::SkyJam::MusicManager::Response::Type, :type, 1
60
+ optional ::SkyJam::MusicManager::Response::AuthStatus, :auth_status, 11
61
+ optional :bool, :auth_error, 12
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
@@ -0,0 +1,11 @@
1
+ ##
2
+ # This file is auto-generated. DO NOT EDIT!
3
+ #
4
+ require 'protobuf/message'
5
+
6
+ module SkyJam
7
+ module MusicManager
8
+ end
9
+
10
+ end
11
+
@@ -0,0 +1,5 @@
1
+ require 'skyjam/music_manager.pb.rb'
2
+ require 'skyjam/music_manager/export_tracks_request.pb.rb'
3
+ require 'skyjam/music_manager/export_tracks_response.pb.rb'
4
+ require 'skyjam/music_manager/auth_request.pb.rb'
5
+ require 'skyjam/music_manager/response.pb.rb'
@@ -0,0 +1,104 @@
1
+ require 'tempfile'
2
+
3
+ module SkyJam
4
+ class Track
5
+ module Source
6
+ STORE = 1
7
+ UPLOAD = 2
8
+ MATCH = 6
9
+ end
10
+
11
+ attr_reader :id,
12
+ :title,
13
+ :album,
14
+ :album_artist,
15
+ :artist,
16
+ :number,
17
+ :size
18
+
19
+ def initialize(info)
20
+ @id = info[:id]
21
+ @title = info[:title]
22
+ @album = info[:album]
23
+ @album_artist = info[:album_artist] unless info[:album_artist].nil? || info[:album_artist].empty?
24
+ @artist = info[:artist]
25
+ @number = info[:track_number]
26
+ @size = info[:track_size]
27
+ end
28
+
29
+ def filename
30
+ escape_path_component("%02d - #{title}" % number) << extname
31
+ end
32
+
33
+ def extname
34
+ '.mp3'
35
+ end
36
+
37
+ def dirname
38
+ path_components = [library.path,
39
+ escape_path_component(album_artist || artist),
40
+ escape_path_component(album)]
41
+ File.join(path_components)
42
+ end
43
+
44
+ def path
45
+ File.join(dirname, filename)
46
+ end
47
+
48
+ def local?
49
+ File.exist?(path)
50
+ end
51
+
52
+ def download(lazy: false)
53
+ return if !lazy || (lazy && local?)
54
+
55
+ file = Tempfile.new(filename)
56
+ begin
57
+ file << data(remote: true)
58
+ rescue SkyJam::Client::Error
59
+ file.close!
60
+ raise
61
+ else
62
+ make_dir
63
+ FileUtils.mv(file.path, path)
64
+ end
65
+ end
66
+
67
+ def data(remote: false)
68
+ if remote || !local?
69
+ url = client.download_url(id)
70
+ client.download_track(url)
71
+ else
72
+ File.binread(path)
73
+ end
74
+ end
75
+
76
+ def upload
77
+ fail NotImplementedError
78
+ end
79
+
80
+ private
81
+
82
+ attr_reader :library
83
+
84
+ def client
85
+ library.send(:client)
86
+ end
87
+
88
+ def make_dir
89
+ FileUtils.mkdir_p(dirname)
90
+ end
91
+
92
+ def escape_path_component(component)
93
+ # OSX: : -> FULLWIDTH COLON (U+FF1A)
94
+ # OSX: / -> : (translated as / in Cocoa)
95
+ # LINUX: / -> DIVISION SLASH (U+2215)
96
+ component = component.dup
97
+
98
+ component.gsub!(':', "\uFF1A")
99
+ component.gsub!('/', ':')
100
+
101
+ component
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,3 @@
1
+ module SkyJam
2
+ VERSION = '0.5.0'
3
+ end
data/lib/skyjam.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'yaml'
4
+ require 'json'
5
+ require 'oauth2'
6
+ require 'protobuf'
7
+
8
+ require 'skyjam/version'
9
+ require 'skyjam/music_manager'
10
+ require 'skyjam/client'
11
+ require 'skyjam/track'
12
+ require 'skyjam/library'
13
+
14
+ module SkyJam
15
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: skyjam
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Loic Nageleisen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: oauth2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rainbow
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: protobuf
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.14'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.14'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.3'
111
+ description: Deftly interact with Google Music (a.k.a Skyjam)
112
+ email:
113
+ - loic.nageleisen@gmail.com
114
+ executables:
115
+ - skyjam
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - LICENSE
120
+ - README.md
121
+ - Rakefile
122
+ - bin/skyjam
123
+ - lib/skyjam.rb
124
+ - lib/skyjam/client.rb
125
+ - lib/skyjam/library.rb
126
+ - lib/skyjam/music_manager.pb.rb
127
+ - lib/skyjam/music_manager.rb
128
+ - lib/skyjam/music_manager/auth_request.pb.rb
129
+ - lib/skyjam/music_manager/export_tracks_request.pb.rb
130
+ - lib/skyjam/music_manager/export_tracks_response.pb.rb
131
+ - lib/skyjam/music_manager/response.pb.rb
132
+ - lib/skyjam/track.rb
133
+ - lib/skyjam/version.rb
134
+ homepage: https://github.com/lloeki/ruby-skyjam
135
+ licenses: []
136
+ metadata: {}
137
+ post_install_message:
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ requirements: []
152
+ rubyforge_project:
153
+ rubygems_version: 2.2.2
154
+ signing_key:
155
+ specification_version: 4
156
+ summary: Google Music API client
157
+ test_files: []