spotify_cli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ require 'dex/ui'
2
+ require 'io/console'
3
+
4
+ module Dex
5
+ module UI
6
+ module Terminal
7
+ def self.width
8
+ if console = IO.console
9
+ console.winsize[1]
10
+ else
11
+ 80
12
+ end
13
+ rescue Errno::EIO
14
+ 80
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/dex/ui.rb ADDED
@@ -0,0 +1,83 @@
1
+ module Dex
2
+ module UI
3
+ autoload :ANSI, 'dex/ui/ansi'
4
+ autoload :Glyph, 'dex/ui/glyph'
5
+ autoload :Color, 'dex/ui/color'
6
+ autoload :Box, 'dex/ui/box'
7
+ autoload :Frame, 'dex/ui/frame'
8
+ autoload :Progress, 'dex/ui/progress'
9
+ autoload :Prompt, 'dex/ui/prompt'
10
+ autoload :Terminal, 'dex/ui/terminal'
11
+ autoload :Formatter, 'dex/ui/formatter'
12
+ autoload :Spinner, 'dex/ui/spinner'
13
+
14
+ # TODO: this, better
15
+ SpinGroup = Spinner::SpinGroup
16
+
17
+ # TODO: test
18
+ def self.glyph(handle)
19
+ Dex::UI::Glyph.lookup(handle)
20
+ end
21
+
22
+ # TODO: test
23
+ def self.resolve_color(input)
24
+ case input
25
+ when Symbol
26
+ Dex::UI::Color.lookup(input)
27
+ else
28
+ input
29
+ end
30
+ end
31
+
32
+ def self.confirm(question)
33
+ Dex::UI::Prompt.confirm(question)
34
+ end
35
+
36
+ def self.ask(question, **kwargs)
37
+ Dex::UI::Prompt.ask(question, **kwargs)
38
+ end
39
+
40
+ def self.resolve_text(input)
41
+ return input if input.nil?
42
+ Dex::UI::Formatter.new(input).format
43
+ end
44
+
45
+ def self.fmt(input, enable_color: true)
46
+ Dex::UI::Formatter.new(input).format(enable_color: enable_color)
47
+ end
48
+
49
+ def self.frame(*args, &block)
50
+ Dex::UI::Frame.open(*args, &block)
51
+ end
52
+
53
+ def self.spinner(*args, &block)
54
+ Dex::UI::Spinner.spin(*args, &block)
55
+ end
56
+
57
+ def self.with_frame_color(color, &block)
58
+ Dex::UI::Frame.with_frame_color_override(color, &block)
59
+ end
60
+
61
+ def self.log_output_to(path)
62
+ if Dex::UI::StdoutRouter.duplicate_output_to
63
+ raise "multiple logs not allowed"
64
+ end
65
+ Dex::UI::StdoutRouter.duplicate_output_to = File.open(path, 'w')
66
+ yield
67
+ ensure
68
+ f = Dex::UI::StdoutRouter.duplicate_output_to
69
+ f.close
70
+ Dex::UI::StdoutRouter.duplicate_output_to = nil
71
+ end
72
+
73
+ def self.raw
74
+ prev = Thread.current[:no_dexui_frame_inset]
75
+ Thread.current[:no_dexui_frame_inset] = true
76
+ yield
77
+ ensure
78
+ Thread.current[:no_dexui_frame_inset] = prev
79
+ end
80
+ end
81
+ end
82
+
83
+ require 'dex/ui/stdout_router'
@@ -0,0 +1,62 @@
1
+ module MethodAddedHook
2
+ private
3
+
4
+ def method_added(meth)
5
+ method_added_hook(meth)
6
+ super
7
+ end
8
+
9
+ def singleton_method_added(meth)
10
+ method_added_hook(meth)
11
+ super
12
+ end
13
+
14
+ def method_added_hook(meth)
15
+ @@__last_defined_doc__ ||= nil
16
+ return if !defined?(@@__last_defined_doc__) || @@__last_defined_doc__.nil?
17
+ @@__class_docs__ ||= {}
18
+ @@__class_docs__[self.to_s] ||= {}
19
+
20
+ @@__class_docs__[self.to_s][meth] = @@__last_defined_doc__
21
+ @@__last_defined_doc__ = nil
22
+ end
23
+ end
24
+
25
+ class Module
26
+ private
27
+ prepend MethodAddedHook
28
+
29
+ def doc(str, meth = nil)
30
+ return @@__class_docs__[self.to_s][meth] = str if meth
31
+ @@__last_defined_doc__ = str
32
+ end
33
+
34
+ def defdoc(str, meth, &block)
35
+ @@__class_docs__[self.to_s][meth] = str
36
+ define_method(meth, &block)
37
+ end
38
+ end
39
+
40
+ module Kernel
41
+ def get_doc(klass, meth)
42
+ docs = klass.class_variable_get(:@@__class_docs__)
43
+ docs[self.to_s][meth.to_sym] if docs && docs[self.to_s]
44
+ end
45
+ end
46
+
47
+
48
+ class String
49
+ # The following methods is taken from activesupport
50
+ #
51
+ # https://github.com/rails/rails/blob/d66e7835bea9505f7003e5038aa19b6ea95ceea1/activesupport/lib/active_support/core_ext/string/strip.rb
52
+ #
53
+ # All credit for this method goes to the original authors.
54
+ # The code is used under the MIT license.
55
+ #
56
+ # Strips indentation by removing the amount of leading whitespace in the least indented
57
+ # non-empty line in the whole string
58
+ #
59
+ def strip_heredoc
60
+ self.gsub(/^#{self.scan(/^[ \t]*(?=\S)/).min}/, "".freeze)
61
+ end
62
+ end
Binary file
@@ -0,0 +1,182 @@
1
+ require 'spotify_cli/app'
2
+ require 'helpers/doc'
3
+
4
+ module SpotifyCli
5
+ class Api
6
+ PLAY = "▶"
7
+ STOP = "◼"
8
+
9
+ SPOTIFY_SEARCH_API = "https://api.spotify.com/v1/search"
10
+
11
+ class << self
12
+ doc <<-EOF
13
+ Changes to the next song
14
+
15
+ {{bold:Usage:}}
16
+ {{command:spotify next}}
17
+ EOF
18
+ def next
19
+ puts "Playing next song"
20
+ SpotifyCli::App.next!
21
+ end
22
+
23
+ doc <<-EOF
24
+ Changes to the previous song
25
+
26
+ {{bold:Usage:}}
27
+ {{command:spotify previous}}
28
+ EOF
29
+ def previous
30
+ puts "Playing previous song"
31
+ SpotifyCli::App.prev!
32
+ end
33
+
34
+ doc <<-EOF
35
+ Sets the position in the song
36
+
37
+ {{bold:Usage:}}
38
+ {{command:spotify set_pos 60}}
39
+ EOF
40
+ def set_pos
41
+ puts "Setting position to #{ARGV[1]}"
42
+ SpotifyCli::App.set_pos!(ARGV[1])
43
+ end
44
+
45
+ doc <<-EOF
46
+ Replays the current song
47
+
48
+ {{bold:Usage:}}
49
+ {{command:spotify replay}}
50
+ EOF
51
+ def replay
52
+ puts "Restarting song"
53
+ SpotifyCli::App.replay!
54
+ end
55
+
56
+ doc <<-EOF
57
+ Play/Pause the current song, or play a specified artist,
58
+ track, album, or uri
59
+
60
+ {{bold:Usage:}}
61
+ {{command:spotify play artist [name]}}
62
+ {{command:spotify play track [name]}}
63
+ {{command:spotify play album [name]}}
64
+ {{command:spotify play uri [spotify uri]}}
65
+ EOF
66
+ def play_pause
67
+ args = ARGV[1..-1]
68
+
69
+ if args.empty?
70
+ # no specifying paremeter, this is a standard play/pause
71
+ SpotifyCli::App.play_pause!
72
+ status
73
+ return
74
+ end
75
+
76
+ arg = args.shift
77
+ type = arg == 'song' ? 'track' : arg
78
+
79
+ Dex::UI.frame("Searching for #{type}", timing: false) do
80
+ play_uri = case type
81
+ when 'album', 'artist', 'track'
82
+ results = search_and_play(type: type, query: args.join(' '))
83
+ results.first
84
+ when 'uri'
85
+ args.first
86
+ end
87
+ puts "Results found, playing"
88
+ SpotifyCli::App.play_uri!(play_uri)
89
+ sleep 0.05 # Give time for the app to switch otherwise status may be stale
90
+ end
91
+
92
+ status
93
+ end
94
+
95
+ doc <<-EOF
96
+ Pause/stop the current song
97
+
98
+ {{bold:Usage:}}
99
+ {{command:spotify pause}}
100
+ {{command:spotify stop}}
101
+ EOF
102
+ def pause
103
+ SpotifyCli::App.pause!
104
+ status
105
+ end
106
+
107
+ doc <<-EOF
108
+ Show the current song
109
+
110
+ {{bold:Usage:}}
111
+ {{command:spotify status}}
112
+ EOF
113
+ def status
114
+ stat = SpotifyCli::App.status
115
+
116
+ time = "#{stat[:position]} / #{stat[:duration]}"
117
+ state_sym = case stat[:state]
118
+ when 'playing'
119
+ PLAY
120
+ else
121
+ STOP
122
+ end
123
+ # 3 for padding around time, and symbol, and space for the symbol, 2 for frame
124
+ width = Dex::UI::Terminal.width - time.size - 5
125
+
126
+ Dex::UI.frame(stat[:track], timing: false) do
127
+ puts Dex::UI.resolve_text([
128
+ "{{bold:Artist:}} #{stat[:artist]}",
129
+ "{{bold:Album:}} #{stat[:album]}",
130
+ ].join("\n"))
131
+ puts [
132
+ Dex::UI::Progress.progress(stat[:percent_done], width),
133
+ state_sym,
134
+ time
135
+ ].join(' ')
136
+ end
137
+ end
138
+
139
+ doc <<-EOF
140
+ Display Help
141
+
142
+ {{bold:Usage:}}
143
+ {{command:spotify}}
144
+ {{command:spotify help}}
145
+ EOF
146
+ def help(mappings)
147
+ Dex::UI.frame('Spotify CLI', timing: false) do
148
+ puts "CLI interface for Spotify"
149
+ end
150
+
151
+ mappings.group_by { |_,v| v }.each do |k, v|
152
+ v.reject! { |mapping| mapping.first == k.to_s }
153
+ doc = get_doc(self.class, k.to_s).strip_heredoc
154
+
155
+ Dex::UI.frame(k, timing: false) do
156
+ puts puts Dex::UI.resolve_text(doc)
157
+ next if v.empty?
158
+ puts Dex::UI.resolve_text("{{bold:Aliases:}}")
159
+ v.each { |mapping| puts Dex::UI.resolve_text(" - {{info:#{mapping.first}}}") }
160
+ end
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ def search_and_play(args)
167
+ type = args[:type]
168
+ type2 = args[:type2] || type
169
+ query = args[:query]
170
+ limit = args[:limit] || 1
171
+ puts "Searching #{type}s for: #{query}";
172
+
173
+ curl_cmd = <<-EOF
174
+ curl -s -G #{SPOTIFY_SEARCH_API} --data-urlencode "q=#{query}" -d "type=#{type}&limit=#{limit}&offset=0" -H "Accept: application/json" \
175
+ | grep -E -o "spotify:#{type2}:[a-zA-Z0-9]+" -m #{limit}
176
+ EOF
177
+
178
+ `#{curl_cmd}`.strip.split("\n")
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,101 @@
1
+ module SpotifyCli
2
+ class App
3
+ def self.state
4
+ oascript('tell application "Spotify" to player state as string')
5
+ end
6
+
7
+ def self.status
8
+ artist = oascript('tell application "Spotify" to artist of current track as string')
9
+ album = oascript('tell application "Spotify" to album of current track as string')
10
+ track = oascript('tell application "Spotify" to name of current track as string')
11
+ duration = oascript(<<-EOF)
12
+ tell application "Spotify"
13
+ set durSec to (duration of current track / 1000) as text
14
+ set tM to (round (durSec / 60) rounding down) as text
15
+ if length of ((durSec mod 60 div 1) as text) is greater than 1 then
16
+ set tS to (durSec mod 60 div 1) as text
17
+ else
18
+ set tS to ("0" & (durSec mod 60 div 1)) as text
19
+ end if
20
+ set myTime to tM as text & ":" & tS as text
21
+ end tell
22
+ return myTime
23
+ EOF
24
+ position = oascript(<<-EOF)
25
+ tell application "Spotify"
26
+ set pos to player position
27
+ set nM to (round (pos / 60) rounding down) as text
28
+ if length of ((round (pos mod 60) rounding down) as text) is greater than 1 then
29
+ set nS to (round (pos mod 60) rounding down) as text
30
+ else
31
+ set nS to ("0" & (round (pos mod 60) rounding down)) as text
32
+ end if
33
+ set nowAt to nM as text & ":" & nS as text
34
+ end tell
35
+ return nowAt
36
+ EOF
37
+
38
+ {
39
+ state: state,
40
+ artist: artist,
41
+ album: album,
42
+ track: track,
43
+ duration: duration,
44
+ position: position,
45
+ percent_done: percent_done(position, duration)
46
+ }
47
+ end
48
+
49
+ def self.play_pause!
50
+ oascript('tell application "Spotify" to playpause')
51
+ end
52
+
53
+ def self.pause!
54
+ oascript('tell application "Spotify" to pause')
55
+ end
56
+
57
+ def self.play_uri!(uri)
58
+ oascript("tell application \"Spotify\" to play track \"#{uri}\"")
59
+ end
60
+
61
+ def self.next!
62
+ oascript('tell application "Spotify" to next track')
63
+ end
64
+
65
+ def self.set_pos!(pos)
66
+ oascript("tell application \"Spotify\" to set player position to #{pos}")
67
+ end
68
+
69
+ def self.previous!
70
+ oascript(<<-EOF)
71
+ tell application "Spotify"
72
+ set player position to 0
73
+ previous track
74
+ end tell
75
+ EOF
76
+ end
77
+
78
+ def self.replay!
79
+ oascript('tell application "Spotify" to set player position to 0')
80
+ end
81
+
82
+ def self.percent_done(position, duration)
83
+ seconds = ->(parts) do
84
+ acc = 0
85
+ multiplier = 1
86
+ while part = parts.shift
87
+ acc += part.to_f * multiplier
88
+ multiplier *= 60
89
+ end
90
+ acc
91
+ end
92
+ pos_parts = position.split(':').reverse
93
+ dur_parts = duration.split(':').reverse
94
+ seconds.call(pos_parts) / seconds.call(dur_parts)
95
+ end
96
+
97
+ def self.oascript(command)
98
+ `osascript -e '#{command}'`.strip
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,3 @@
1
+ module SpotifyCli
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,38 @@
1
+ require 'spotify_cli/version'
2
+ require 'dex/ui'
3
+ require 'spotify_cli/api'
4
+
5
+ module SpotifyCli
6
+ def self.call(args)
7
+ mappings = {
8
+ 'next' => :next,
9
+ 'n' => :next,
10
+ 'previous' => :previous,
11
+ 'pr' => :previous,
12
+ 'set_pos' => :set_pos,
13
+ 'pos' => :set_pos,
14
+ 'replay' => :replay,
15
+ 'rep' => :replay,
16
+ 'restart' => :replay,
17
+ 'pause' => :pause,
18
+ 'stop' => :pause,
19
+ 'play' => :play_pause,
20
+ 'p' => :play_pause,
21
+ 'play_pause' => :play_pause,
22
+ 'status' => :status,
23
+ 's' => :status,
24
+ 'help' => :help
25
+ }
26
+
27
+ if args.empty?
28
+ SpotifyCli::Api.status
29
+ else
30
+ mapping = mappings[args.first]
31
+ if mapping.nil? || mapping == :help
32
+ SpotifyCli::Api.help(mappings)
33
+ else
34
+ SpotifyCli::Api.send(mapping)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'spotify_cli/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "spotify_cli"
8
+ spec.version = SpotifyCli::VERSION
9
+ spec.authors = ["Julian Nadeau"]
10
+ spec.email = ["julian@jnadeau.ca"]
11
+ spec.license = "MIT"
12
+
13
+ spec.summary = "Spotify Application wrapper for control via command line"
14
+ spec.description = "Allow control of Spotify using a pretty UI interface. Intentionally simple."
15
+ spec.homepage = "https://github.com/jules2689/spotify_cli"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against " \
23
+ "public gem pushes."
24
+ end
25
+
26
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
27
+ f.match(%r{^(test|spec|features)/})
28
+ end
29
+ spec.bindir = "bin"
30
+ spec.executables = ['spotify']
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_development_dependency "bundler", "~> 1.14"
34
+ spec.add_development_dependency "rake", "~> 10.0"
35
+ spec.add_development_dependency "minitest", "~> 5.0"
36
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spotify_cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Julian Nadeau
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-05-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ description: Allow control of Spotify using a pretty UI interface. Intentionally simple.
56
+ email:
57
+ - julian@jnadeau.ca
58
+ executables:
59
+ - spotify
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".DS_Store"
64
+ - ".gitignore"
65
+ - ".travis.yml"
66
+ - CODE_OF_CONDUCT.md
67
+ - Gemfile
68
+ - README.md
69
+ - Rakefile
70
+ - bin/console
71
+ - bin/setup
72
+ - bin/spotify
73
+ - lib/dex/ui.rb
74
+ - lib/dex/ui/README.md
75
+ - lib/dex/ui/ansi.rb
76
+ - lib/dex/ui/box.rb
77
+ - lib/dex/ui/color.rb
78
+ - lib/dex/ui/formatter.rb
79
+ - lib/dex/ui/frame.rb
80
+ - lib/dex/ui/glyph.rb
81
+ - lib/dex/ui/progress.rb
82
+ - lib/dex/ui/prompt.rb
83
+ - lib/dex/ui/spinner.rb
84
+ - lib/dex/ui/stdout_router.rb
85
+ - lib/dex/ui/terminal.rb
86
+ - lib/helpers/doc.rb
87
+ - lib/spotify_cli.rb
88
+ - lib/spotify_cli/.DS_Store
89
+ - lib/spotify_cli/api.rb
90
+ - lib/spotify_cli/app.rb
91
+ - lib/spotify_cli/version.rb
92
+ - spotify_cli.gemspec
93
+ homepage: https://github.com/jules2689/spotify_cli
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ allowed_push_host: https://rubygems.org
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 2.5.1
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: Spotify Application wrapper for control via command line
118
+ test_files: []