shellify 1.0.0 → 1.1.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 +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +4 -19
- data/lib/shellify/cli.rb +26 -0
- data/lib/shellify/config.rb +24 -16
- data/lib/shellify/oauth_callback_handler.rb +68 -0
- data/lib/shellify/user.rb +30 -16
- data/lib/shellify/utils.rb +12 -0
- data/lib/shellify/version.rb +1 -1
- data/lib/shellify.rb +4 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 314e6a4441fcb3d6f77285ac44ecd3a5d2d77f04fe015aaefea06ac489f75813
|
4
|
+
data.tar.gz: 7cf9f6ac9154daab30f54797ada008e80cf5c29914fef6fdf91abf0bd392cc2b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8e2a4ad6c4bd8fb83a6e4c8f3af3feec80c05e4ac8e7075e6fb0b9dd09c4b12e97012b7deb8cc672553a2b581fc0d72adf40cb99beee21334f111dc9777c594f
|
7
|
+
data.tar.gz: 4e44e84dc5dd2fdc9b305636f04a170bf99faf0d88a8a40c158656817ff8b9c28a5ec872eb7d7bed5711a4f8a0c9fab4efcb8bc5e94ba7a4b16000e2ee103eb7
|
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2022 Derek Povah <
|
3
|
+
Copyright (c) 2022 Derek Povah <https://derekpovah.com>
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
@@ -14,25 +14,10 @@ $ gem install shellify
|
|
14
14
|
|
15
15
|
#### Setup
|
16
16
|
|
17
|
-
1. Create
|
18
|
-
1.
|
19
|
-
1.
|
20
|
-
|
21
|
-
```json
|
22
|
-
{
|
23
|
-
"client_id": "xxxxxxxxxxxxxxxx",
|
24
|
-
"client_secret": "xxxxxxxxxxxxxxx"
|
25
|
-
}
|
26
|
-
```
|
27
|
-
|
28
|
-
`spotify_user.json`
|
29
|
-
```json
|
30
|
-
{
|
31
|
-
"id": "spotify_user_id",
|
32
|
-
"token": "xxxxxxxxxxxxxxx",
|
33
|
-
"refresh_token": "xxxxxxxxxxxxxxx"
|
34
|
-
}
|
35
|
-
```
|
17
|
+
1. Create an application on the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard).
|
18
|
+
1. Add `http://localhost:8888/callback` to the Spotify Application's Redirect URIs
|
19
|
+
1. Run `shellify configure`
|
20
|
+
1. Run `shellify authenticate`
|
36
21
|
|
37
22
|
Commands
|
38
23
|
--------
|
data/lib/shellify/cli.rb
CHANGED
@@ -19,6 +19,32 @@ module Shellify
|
|
19
19
|
program :version, Shellify::VERSION
|
20
20
|
program :description, 'Use Spotify from the command line'
|
21
21
|
|
22
|
+
command :configure do |c|
|
23
|
+
c.description = 'Set the Spotify client_id and client_secret'
|
24
|
+
c.action do
|
25
|
+
client_id = ask("Spotify Client ID: ")
|
26
|
+
client_secret = ask("Spotify Client Secret: ") { |q| q.echo = '*' }
|
27
|
+
@config.client_id = client_id
|
28
|
+
@config.client_secret = client_secret
|
29
|
+
@config.save!
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
command :authenticate do |c|
|
34
|
+
c.description = 'Authenticate with the Spotify API'
|
35
|
+
c.action do
|
36
|
+
spotify_username = ask('Your Spotify Username: ')
|
37
|
+
puts
|
38
|
+
puts 'Go to the link below to authorize Shellify.'
|
39
|
+
puts generate_oauth_url
|
40
|
+
oauth_credentials = Shellify::OauthCallbackHandler.run(@config)
|
41
|
+
@user.id = spotify_username
|
42
|
+
@user.token = oauth_credentials['access_token']
|
43
|
+
@user.refresh_token = oauth_credentials['refresh_token']
|
44
|
+
@user.save!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
22
48
|
command :devices do |c|
|
23
49
|
c.description = 'List available playback devices'
|
24
50
|
c.action do
|
data/lib/shellify/config.rb
CHANGED
@@ -2,37 +2,45 @@
|
|
2
2
|
|
3
3
|
module Shellify
|
4
4
|
class Config
|
5
|
-
|
5
|
+
attr_accessor :client_id, :client_secret, :config_dir
|
6
6
|
|
7
7
|
CONFIG_DIR = ENV['HOME'] + '/.config/shellify'
|
8
8
|
CONFIG_FILE = CONFIG_DIR + '/config.json'
|
9
|
+
SPOTIFY_AUTHORIZATION_SCOPES = %w[
|
10
|
+
user-read-playback-state
|
11
|
+
user-modify-playback-state
|
12
|
+
user-read-currently-playing
|
13
|
+
user-library-modify
|
14
|
+
user-library-read
|
15
|
+
playlist-modify-private
|
16
|
+
playlist-read-collaborative
|
17
|
+
playlist-read-private
|
18
|
+
playlist-modify-public
|
19
|
+
].join(' ')
|
9
20
|
|
10
21
|
def initialize
|
11
22
|
@config_dir = CONFIG_DIR
|
12
23
|
@config_file = CONFIG_FILE
|
13
|
-
create_config_file
|
14
24
|
load_config
|
15
|
-
RSpotify.authenticate(@client_id, @client_secret)
|
25
|
+
RSpotify.authenticate(@client_id, @client_secret) if configured?
|
16
26
|
end
|
17
27
|
|
18
|
-
|
28
|
+
def configured?
|
29
|
+
!@client_id.nil? && !@client_secret.nil?
|
30
|
+
end
|
19
31
|
|
20
|
-
def
|
21
|
-
|
32
|
+
def save!
|
33
|
+
File.open(CONFIG_FILE, 'w') do |file|
|
34
|
+
file.write(JSON.pretty_generate({client_id: @client_id, client_secret: @client_secret}))
|
35
|
+
end
|
22
36
|
end
|
23
37
|
|
24
|
-
|
25
|
-
return if File.exists?(CONFIG_FILE)
|
38
|
+
private
|
26
39
|
|
27
|
-
|
28
|
-
|
29
|
-
write_default_config
|
30
|
-
end
|
40
|
+
def load_config
|
41
|
+
return unless File.exists?(CONFIG_FILE)
|
31
42
|
|
32
|
-
|
33
|
-
File.open(CONFIG_FILE, 'w') do |file|
|
34
|
-
file.write(JSON.pretty_generate({client_id: '', client_secret: ''}))
|
35
|
-
end
|
43
|
+
JSON.parse(File.read(CONFIG_FILE)).each_pair { |k,v| instance_variable_set("@#{k}", v) }
|
36
44
|
end
|
37
45
|
end
|
38
46
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'cgi'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
module Shellify
|
8
|
+
class OauthCallbackHandler
|
9
|
+
def self.run(...)
|
10
|
+
new(...).run
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
@server = TCPServer.open(8888)
|
19
|
+
@client = @server.accept
|
20
|
+
|
21
|
+
path = @client.gets.split[1]
|
22
|
+
params = CGI.parse(path.split('?').last).transform_values(&:first)
|
23
|
+
body = 'Success! (You can close this now)'
|
24
|
+
|
25
|
+
begin
|
26
|
+
tokens = fetch_tokens(params['code'])
|
27
|
+
rescue RestClient::Exception => e
|
28
|
+
body = "Spotify didn't like that\n" + e.response
|
29
|
+
end
|
30
|
+
|
31
|
+
@client.puts headers(body.length)
|
32
|
+
@client.puts body
|
33
|
+
tokens
|
34
|
+
ensure
|
35
|
+
@client.close if @client
|
36
|
+
@server.close
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def headers(content_length)
|
42
|
+
[
|
43
|
+
'HTTP/1.1 200 Ok',
|
44
|
+
"date: #{Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S GMT")}",
|
45
|
+
'server: ruby',
|
46
|
+
"Content-Length: #{content_length}",
|
47
|
+
'',
|
48
|
+
'',
|
49
|
+
].join("\r\n")
|
50
|
+
end
|
51
|
+
|
52
|
+
def fetch_tokens(code)
|
53
|
+
headers = {
|
54
|
+
'Authorization': "Basic " + Base64.strict_encode64("#{@config.client_id}:#{@config.client_secret}"),
|
55
|
+
}
|
56
|
+
|
57
|
+
params = {
|
58
|
+
client_id: @config.client_id,
|
59
|
+
scope: Shellify::Config::SPOTIFY_AUTHORIZATION_SCOPES,
|
60
|
+
redirect_uri: 'http://localhost:8888/callback',
|
61
|
+
grant_type: 'authorization_code',
|
62
|
+
code: code,
|
63
|
+
}
|
64
|
+
|
65
|
+
JSON.parse(RestClient.post("https://accounts.spotify.com/api/token", params, headers))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/shellify/user.rb
CHANGED
@@ -2,48 +2,62 @@
|
|
2
2
|
|
3
3
|
module Shellify
|
4
4
|
class User < RSpotify::User
|
5
|
+
attr_accessor :token, :refresh_token, :id
|
5
6
|
|
6
7
|
USER_FILE = '/spotify_user.json'
|
7
8
|
|
8
9
|
def initialize(config_dir)
|
9
10
|
@config_dir = config_dir
|
11
|
+
@user_file_path = config_dir + USER_FILE
|
10
12
|
create_user_file
|
11
|
-
|
13
|
+
write_default_user
|
14
|
+
load_persisted_user
|
12
15
|
super({
|
13
16
|
'credentials' => {
|
14
|
-
'token' => @
|
15
|
-
'refresh_token' => @
|
17
|
+
'token' => @token,
|
18
|
+
'refresh_token' => @refresh_token,
|
16
19
|
'access_refresh_callback' => access_refresh_callback,
|
17
20
|
},
|
18
|
-
'id' => @
|
21
|
+
'id' => @id,
|
19
22
|
})
|
20
23
|
end
|
21
24
|
|
22
|
-
|
25
|
+
def configured?
|
26
|
+
!@token.empty? && !@refresh_token.empty? && !@id.empty?
|
27
|
+
end
|
23
28
|
|
24
|
-
def
|
25
|
-
|
29
|
+
def save!
|
30
|
+
File.open(@user_file_path, 'w') do |file|
|
31
|
+
file.write(JSON.pretty_generate({id: @id, token: @token, refresh_token: @refresh_token}))
|
32
|
+
end
|
26
33
|
end
|
27
34
|
|
28
|
-
|
29
|
-
@spotify_user.token = access_token
|
35
|
+
private
|
30
36
|
|
31
|
-
|
32
|
-
|
33
|
-
end
|
37
|
+
def load_persisted_user
|
38
|
+
JSON.parse(File.read(@user_file_path)).each_pair { |k,v| instance_variable_set("@#{k}", v) }
|
34
39
|
end
|
35
40
|
|
36
41
|
def access_refresh_callback
|
37
42
|
Proc.new do |new_access_token, _token_lifetime|
|
38
|
-
|
43
|
+
@token = new_access_token
|
44
|
+
save!
|
39
45
|
end
|
40
46
|
end
|
41
47
|
|
42
48
|
def create_user_file
|
43
|
-
return if File.exists?(@
|
49
|
+
return if File.exists?(@user_file_path)
|
50
|
+
|
51
|
+
FileUtils.mkdir_p(@config_dir)
|
52
|
+
FileUtils.touch(@user_file_path)
|
53
|
+
end
|
44
54
|
|
45
|
-
|
46
|
-
|
55
|
+
def write_default_user
|
56
|
+
return unless File.zero?(@user_file_path)
|
57
|
+
|
58
|
+
File.open(@user_file_path, 'w') do |file|
|
59
|
+
file.write(JSON.pretty_generate({id: '', token: '', refresh_token: '',}))
|
60
|
+
end
|
47
61
|
end
|
48
62
|
end
|
49
63
|
end
|
data/lib/shellify/utils.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'time'
|
4
|
+
require 'uri'
|
4
5
|
|
5
6
|
module Shellify
|
6
7
|
module Utils
|
@@ -15,5 +16,16 @@ module Shellify
|
|
15
16
|
def time_to_ms(time)
|
16
17
|
time.split(':').map { |a| a.to_i }.inject(0) { |a, b| a * 60 + b} * 1000
|
17
18
|
end
|
19
|
+
|
20
|
+
def generate_oauth_url
|
21
|
+
url_params = {
|
22
|
+
response_type: 'code',
|
23
|
+
client_id: @config.client_id,
|
24
|
+
scope: Shellify::Config::SPOTIFY_AUTHORIZATION_SCOPES,
|
25
|
+
redirect_uri: 'http://localhost:8888/callback',
|
26
|
+
}
|
27
|
+
|
28
|
+
"https://accounts.spotify.com/authorize?#{URI.encode_www_form(url_params)}"
|
29
|
+
end
|
18
30
|
end
|
19
31
|
end
|
data/lib/shellify/version.rb
CHANGED
data/lib/shellify.rb
CHANGED
@@ -6,7 +6,8 @@ require 'rspotify'
|
|
6
6
|
require 'shellify/version'
|
7
7
|
|
8
8
|
module Shellify
|
9
|
-
autoload :Cli,
|
10
|
-
autoload :Config,
|
11
|
-
autoload :User,
|
9
|
+
autoload :Cli, 'shellify/cli'
|
10
|
+
autoload :Config, 'shellify/config'
|
11
|
+
autoload :User, 'shellify/user'
|
12
|
+
autoload :OauthCallbackHandler, 'shellify/oauth_callback_handler'
|
12
13
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shellify
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Derek Povah
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-01-
|
11
|
+
date: 2022-01-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: commander
|
@@ -80,6 +80,7 @@ files:
|
|
80
80
|
- lib/shellify.rb
|
81
81
|
- lib/shellify/cli.rb
|
82
82
|
- lib/shellify/config.rb
|
83
|
+
- lib/shellify/oauth_callback_handler.rb
|
83
84
|
- lib/shellify/user.rb
|
84
85
|
- lib/shellify/utils.rb
|
85
86
|
- lib/shellify/version.rb
|