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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dc23ed10c36ddf92d2881d109271119d606b8ea380183a1d277353f13693f16
4
- data.tar.gz: d66f3374654de5851cf5565a4c2c1ede72971c80a223a2748cc8af6d2f25daf7
3
+ metadata.gz: 314e6a4441fcb3d6f77285ac44ecd3a5d2d77f04fe015aaefea06ac489f75813
4
+ data.tar.gz: 7cf9f6ac9154daab30f54797ada008e80cf5c29914fef6fdf91abf0bd392cc2b
5
5
  SHA512:
6
- metadata.gz: 32d3d1b078e5fe7fa1877cefed381104ce05df8ac2d72b3a23ef984ebba6efac5a3e37e2f6de70c5ece2495dd16cec8228a6db385235d43a67309d78da0a0f6e
7
- data.tar.gz: 8d80c7a94c9ebc89408236324e3b2f6f2f10f3ce15bb26b88a012ac9ac5d91eb6e9f198553f34646e1f43d92341a35e81f3103b04c77ce64abd4d8e513e803cd
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 <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
@@ -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.0'
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.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-28 00:00:00.000000000 Z
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