shellify 1.0.0 → 1.1.2
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 +55 -6
- 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: 0b457f8f07a6f855b4cbb3a2839df79780b49760ad264247b6225cb5fe7d0be3
|
4
|
+
data.tar.gz: 5e96c98b637eccc3f1fc4e24808baf60db90deca6eb061810a5db7bddc2d14ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd69fbf66ede7b711ca6d5c24d568272f73ee2cc4303e8a4900dac5f525118e96e9c2e9f1de69e8dad4cf9268d4da103cfc07d9454e5e6c87fab7622135d7ae6
|
7
|
+
data.tar.gz: f162600a265e3e6167caf78480eb9d640a1089a93c6de8deb9739b9e9ea13b639cbc617157eec14cb468cf027a5f40a083957dd651a314346f839aa53c9df162
|
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
|
@@ -48,14 +74,16 @@ module Shellify
|
|
48
74
|
command :like do |c|
|
49
75
|
c.description = 'Save the current song to your library'
|
50
76
|
c.action do
|
51
|
-
|
77
|
+
exit_with_message(local_track_message, 0) if track_is_local?(playing)
|
78
|
+
@user.save_tracks!([playing])
|
52
79
|
end
|
53
80
|
end
|
54
81
|
|
55
82
|
command :unlike do |c|
|
56
83
|
c.description = 'Remove the current song from your library'
|
57
84
|
c.action do
|
58
|
-
|
85
|
+
exit_with_message(local_track_message, 0) if track_is_local?(playing)
|
86
|
+
@user.remove_tracks!([playing])
|
59
87
|
end
|
60
88
|
end
|
61
89
|
|
@@ -73,10 +101,12 @@ module Shellify
|
|
73
101
|
c.action do |args, options|
|
74
102
|
return puts " Nothing playing" unless @user.player.playing?
|
75
103
|
|
104
|
+
exit_with_message(local_track_message, 0) if track_is_local?(playing)
|
76
105
|
playlist = @user.playlists.find { |p| p.name == args[0] }
|
77
106
|
return puts " Playlist not found" unless playlist
|
107
|
+
exit_with_message(add_to_collaborative_playlist_message, 0) if playlist.owner != @user
|
78
108
|
|
79
|
-
playlist.add_tracks!([
|
109
|
+
playlist.add_tracks!([playing])
|
80
110
|
end
|
81
111
|
end
|
82
112
|
|
@@ -85,10 +115,12 @@ module Shellify
|
|
85
115
|
c.action do |args, options|
|
86
116
|
return puts " Nothing playing" unless @user.player.playing?
|
87
117
|
|
118
|
+
exit_with_message(local_track_message, 0) if track_is_local?(playing)
|
88
119
|
playlist = @user.playlists.find { |p| p.name == args[0] }
|
89
120
|
return puts " Playlist not found" unless playlist
|
121
|
+
exit_with_message(add_to_collaborative_playlist_message, 0) if playlist.owner != @user
|
90
122
|
|
91
|
-
playlist.remove_tracks!([
|
123
|
+
playlist.remove_tracks!([playing])
|
92
124
|
end
|
93
125
|
end
|
94
126
|
|
@@ -150,12 +182,29 @@ module Shellify
|
|
150
182
|
|
151
183
|
private
|
152
184
|
|
185
|
+
def playing
|
186
|
+
@user.player.currently_playing
|
187
|
+
end
|
188
|
+
|
189
|
+
def local_track_message
|
190
|
+
" Shellify can't perform this action for local tracks"
|
191
|
+
end
|
192
|
+
|
193
|
+
def add_to_collaborative_playlist_message
|
194
|
+
" Shellify can't perform this action for collaborative playlists you don't own"
|
195
|
+
end
|
196
|
+
|
197
|
+
def track_is_local?(track)
|
198
|
+
track.uri.split(':')[1] == 'local'
|
199
|
+
end
|
200
|
+
|
153
201
|
def print_current_song
|
154
|
-
playing = @user.player.currently_playing
|
155
202
|
puts ' Now Playing:'
|
156
203
|
puts " #{playing.name} - #{playing.artists.first.name} - "\
|
157
204
|
"#{duration_to_s(@user.player.progress)}/#{duration_to_s(playing.duration_ms)}"\
|
158
|
-
"#{" - ♥" if @user.saved_tracks?([playing]).first}"
|
205
|
+
"#{" - ♥" if !track_is_local?(playing) && @user.saved_tracks?([playing]).first}"\
|
206
|
+
"#{" - local" if track_is_local?(playing)}"
|
207
|
+
|
159
208
|
end
|
160
209
|
|
161
210
|
def exit_with_message(message, code = 1)
|
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.2
|
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-
|
11
|
+
date: 2022-03-29 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
|