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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dc23ed10c36ddf92d2881d109271119d606b8ea380183a1d277353f13693f16
4
- data.tar.gz: d66f3374654de5851cf5565a4c2c1ede72971c80a223a2748cc8af6d2f25daf7
3
+ metadata.gz: 0b457f8f07a6f855b4cbb3a2839df79780b49760ad264247b6225cb5fe7d0be3
4
+ data.tar.gz: 5e96c98b637eccc3f1fc4e24808baf60db90deca6eb061810a5db7bddc2d14ce
5
5
  SHA512:
6
- metadata.gz: 32d3d1b078e5fe7fa1877cefed381104ce05df8ac2d72b3a23ef984ebba6efac5a3e37e2f6de70c5ece2495dd16cec8228a6db385235d43a67309d78da0a0f6e
7
- data.tar.gz: 8d80c7a94c9ebc89408236324e3b2f6f2f10f3ce15bb26b88a012ac9ac5d91eb6e9f198553f34646e1f43d92341a35e81f3103b04c77ce64abd4d8e513e803cd
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 <http://derekpovah.com>
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 a Spotify application in the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard).
18
- 1. Get your Spotify OAUTH `access_token` and `refresh_token`. (Shelilfy currently doesn't currently implement a two way OAUTH flow, but Spotify has [sample apps](https://github.com/spotify/web-api-auth-examples) that you can use to get your initial keys. Shellify will handle exchanging refresh tokens for access tokens after initial setup.)
19
- 1. Create two json files in `~/.config/shellify`
20
- `config.json`
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
- @user.save_tracks!([@user.player.currently_playing])
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
- @user.remove_tracks!([@user.player.currently_playing])
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!([@user.player.currently_playing])
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!([@user.player.currently_playing])
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)
@@ -2,37 +2,45 @@
2
2
 
3
3
  module Shellify
4
4
  class Config
5
- attr_reader :client_id, :client_secret, :config_dir
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
- private
28
+ def configured?
29
+ !@client_id.nil? && !@client_secret.nil?
30
+ end
19
31
 
20
- def load_config
21
- JSON.parse(File.read(CONFIG_FILE)).each_pair { |k,v| instance_variable_set("@#{k}", v) }
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
- def create_config_file
25
- return if File.exists?(CONFIG_FILE)
38
+ private
26
39
 
27
- FileUtils.mkdir_p(CONFIG_DIR)
28
- FileUtils.touch(CONFIG_FILE)
29
- write_default_config
30
- end
40
+ def load_config
41
+ return unless File.exists?(CONFIG_FILE)
31
42
 
32
- def write_default_config
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
- @spotify_user = load_persisted_user
13
+ write_default_user
14
+ load_persisted_user
12
15
  super({
13
16
  'credentials' => {
14
- 'token' => @spotify_user.token,
15
- 'refresh_token' => @spotify_user.refresh_token,
17
+ 'token' => @token,
18
+ 'refresh_token' => @refresh_token,
16
19
  'access_refresh_callback' => access_refresh_callback,
17
20
  },
18
- 'id' => @spotify_user.id,
21
+ 'id' => @id,
19
22
  })
20
23
  end
21
24
 
22
- private
25
+ def configured?
26
+ !@token.empty? && !@refresh_token.empty? && !@id.empty?
27
+ end
23
28
 
24
- def load_persisted_user
25
- OpenStruct.new(JSON.parse(File.read(@config_dir + USER_FILE)))
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
- def persist_user(access_token)
29
- @spotify_user.token = access_token
35
+ private
30
36
 
31
- File.open(@config_dir + USER_FILE, 'w') do |file|
32
- file.write(JSON.pretty_generate(@spotify_user.to_h))
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
- persist_user(new_access_token)
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?(@config_dir + USER_FILE)
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
- FileUtils.mkdir_p(CONFIG_DIR)
46
- FileUtils.touch(@config_dir + USER_FILE)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shellify
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.2'
5
5
  end
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, 'shellify/cli'
10
- autoload :Config, 'shellify/config'
11
- autoload :User, 'shellify/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.0.0
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-01-28 00:00:00.000000000 Z
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