skyjam 0.5.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 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: []