skyjam 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +24 -0
- data/README.md +86 -0
- data/Rakefile +6 -0
- data/bin/skyjam +51 -0
- data/lib/skyjam/client.rb +289 -0
- data/lib/skyjam/library.rb +77 -0
- data/lib/skyjam/music_manager/auth_request.pb.rb +26 -0
- data/lib/skyjam/music_manager/export_tracks_request.pb.rb +35 -0
- data/lib/skyjam/music_manager/export_tracks_response.pb.rb +50 -0
- data/lib/skyjam/music_manager/response.pb.rb +67 -0
- data/lib/skyjam/music_manager.pb.rb +11 -0
- data/lib/skyjam/music_manager.rb +5 -0
- data/lib/skyjam/track.rb +104 -0
- data/lib/skyjam/version.rb +3 -0
- data/lib/skyjam.rb +15 -0
- metadata +157 -0
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
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
|
+
|
data/lib/skyjam/track.rb
ADDED
@@ -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
|
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: []
|