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 +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: []
|