terminal_player 0.0.7

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.
@@ -0,0 +1,204 @@
1
+ require 'spotify'
2
+ require 'plaything'
3
+
4
+ class SpotiphyPlayer
5
+ include Observable
6
+
7
+ def initialize(options)
8
+ @options = options
9
+
10
+ @plaything = Plaything.new
11
+ setup_session_callbacks
12
+
13
+ # HACK not sure this is copacetic
14
+ path = File.expand_path File.dirname(__FILE__)
15
+ path = "#{path}/../../../"
16
+ appkey = IO.read("#{path}spotify_appkey.key", encoding: "BINARY")
17
+ config = Spotify::SessionConfig.new({
18
+ api_version: Spotify::API_VERSION.to_i,
19
+ application_key: appkey,
20
+ cache_location: "#{path}.spotify/",
21
+ settings_location: "#{path}.spotify/",
22
+ tracefile: "#{path}spotify_tracefile.txt",
23
+ user_agent: "terminal_player gem",
24
+ callbacks: Spotify::SessionCallbacks.new($session_callbacks),
25
+ })
26
+ @session = create_session(config)
27
+
28
+ login(get_env("SPOTIFY_USERNAME"), get_env("SPOTIFY_PASSWORD"))
29
+ end
30
+
31
+ def play
32
+ u = @options[:url]
33
+ if u[':track:']
34
+ play_track_from_uri u
35
+ elsif u[':playlist:']
36
+ play_playlist u
37
+ elsif u[':album:']
38
+ play_album u
39
+ else
40
+ puts "unsupported URI #{u}"
41
+ end
42
+ end
43
+
44
+ def next
45
+ $end_of_track = true
46
+ end
47
+
48
+ def get_track(uri)
49
+ link = Spotify.link_create_from_string(uri)
50
+ track = Spotify.link_as_track(link)
51
+ track
52
+ end
53
+
54
+ def play_track_from_uri(uri)
55
+ track = get_track(uri)
56
+ play_track track
57
+ end
58
+
59
+ def play_track(track)
60
+ wait_for_track_to_load track
61
+ artist = Spotify.track_artist(track, 0)
62
+ notify "SPOTTY #{Spotify.track_name(track)} - #{Spotify.artist_name(artist)}"
63
+ begin
64
+ play_track_raw track
65
+ wait_for_track_to_end
66
+ rescue => e
67
+ puts "play_track: error playing track: #{e}"
68
+ end
69
+ end
70
+
71
+ def play_playlist(uri)
72
+ link = Spotify.link_create_from_string(uri)
73
+ plist = Spotify.playlist_create(@session, link)
74
+ poll(@session) { Spotify.playlist_is_loaded(plist) }
75
+ num_tracks = Spotify.playlist_num_tracks(plist)
76
+ # TODO this should reset channel
77
+ # puts "\nPlaying #{Spotify.playlist_name(plist)}, #{num_tracks} tracks, " +
78
+ # "#{Spotify.playlist_num_subscribers(plist)} subscribers"
79
+ 0.upto(num_tracks - 1) do |i|
80
+ track = Spotify.playlist_track(plist, i)
81
+ play_track track
82
+ end
83
+ end
84
+
85
+ def play_album(uri)
86
+ link = Spotify.link_create_from_string(uri)
87
+ browser = Spotify.albumbrowse_create(@session, Spotify.link_as_album(link), proc { }, nil)
88
+ poll(@session) { Spotify.albumbrowse_is_loaded(browser) }
89
+ # album = Spotify.albumbrowse_album(browser)
90
+ num_tracks = Spotify.albumbrowse_num_tracks(browser)
91
+ # TODO this should reset channel
92
+ # puts "\nPlaying #{Spotify.album_name(album)} (#{Spotify.album_year(album)}), #{num_tracks} tracks"
93
+ 0.upto(num_tracks - 1) do |i|
94
+ track = Spotify.albumbrowse_track(browser, i)
95
+ play_track track
96
+ end
97
+ end
98
+
99
+ def play_track_raw(track)
100
+ Spotify.try(:session_player_play, @session, false)
101
+ Spotify.try(:session_player_load, @session, track)
102
+ Spotify.try(:session_player_play, @session, true)
103
+ end
104
+
105
+ def wait_for_track_to_end
106
+ poll(@session) { $end_of_track }
107
+ $end_of_track = false
108
+ end
109
+
110
+ def wait_for_track_to_load(track)
111
+ poll(@session) { Spotify.track_is_loaded(track) }
112
+ end
113
+
114
+ private
115
+
116
+ def notify(message)
117
+ changed
118
+ notify_observers(Time.now, message)
119
+ end
120
+
121
+ def login(u, p)
122
+ Spotify.session_login(@session, u, p, false, nil)
123
+ poll(@session) { Spotify.session_connectionstate(@session) == :logged_in }
124
+ end
125
+
126
+ def poll(session)
127
+ until yield
128
+ FFI::MemoryPointer.new(:int) do |ptr|
129
+ Spotify.session_process_events(session, ptr)
130
+ end
131
+ sleep(0.1)
132
+ end
133
+ end
134
+
135
+ def create_session(config)
136
+ FFI::MemoryPointer.new(Spotify::Session) do |ptr|
137
+ Spotify.try(:session_create, config, ptr)
138
+ return Spotify::Session.new(ptr.read_pointer)
139
+ end
140
+ end
141
+
142
+ def setup_session_callbacks
143
+ # these must remain global. i think.
144
+ $session_callbacks = {
145
+ log_message: proc do |session, message|
146
+ #$logger.info("session (log message)") { message }
147
+ end,
148
+
149
+ logged_in: proc do |session, error|
150
+ #$logger.debug("session (logged in)") { Spotify::Error.explain(error) }
151
+ end,
152
+
153
+ logged_out: proc do |session|
154
+ #$logger.debug("session (logged out)") { "logged out!" }
155
+ end,
156
+
157
+ streaming_error: proc do |session, error|
158
+ #$logger.error("session (player)") { "streaming error %s" % Spotify::Error.explain(error) }
159
+ end,
160
+
161
+ start_playback: proc do |session|
162
+ #$logger.debug("session (player)") { "start playback" }
163
+ @plaything.play
164
+ end,
165
+
166
+ stop_playback: proc do |session|
167
+ #$logger.debug("session (player)") { "stop playback" }
168
+ @plaything.stop
169
+ end,
170
+
171
+ get_audio_buffer_stats: proc do |session, stats|
172
+ stats[:samples] = @plaything.queue_size
173
+ stats[:stutter] = @plaything.drops
174
+ #$logger.debug("session (player)") { "queue size [#{stats[:samples]}, #{stats[:stutter]}]" }
175
+ end,
176
+
177
+ music_delivery: proc do |session, format, frames, num_frames|
178
+ if num_frames == 0
179
+ #$logger.debug("session (player)") { "music delivery audio discontuity" }
180
+ @plaything.stop
181
+ 0
182
+ else
183
+ frames = FrameReader.new(format[:channels], format[:sample_type], num_frames, frames)
184
+ consumed_frames = @plaything.stream(frames, format.to_h)
185
+ #$logger.debug("session (player)") { "music delivery #{consumed_frames} of #{num_frames}" }
186
+ consumed_frames
187
+ end
188
+ end,
189
+
190
+ end_of_track: proc do |session|
191
+ $end_of_track = true
192
+ #$logger.debug("session (player)") { "end of track" }
193
+ @plaything.stop
194
+ end,
195
+ }
196
+ end
197
+
198
+ def get_env(name)
199
+ ENV.fetch(name) do
200
+ raise "set the #{name} environment variable"
201
+ end
202
+ end
203
+ end
204
+
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+ require 'terminal_player/di'
3
+
4
+ describe AudioAddict do
5
+ class DummyClass
6
+ end
7
+
8
+ before(:each) do
9
+ @dummy = DummyClass.new
10
+ @dummy.extend(AudioAddict)
11
+ end
12
+
13
+ it "returns a good song list" do
14
+ songs = @dummy.get_recently_played_list(15) # 15 is breaks
15
+ songs.length.should be > 0
16
+ s = songs[0]
17
+ s.should.respond_to? :channel_id
18
+ s['channel_id'].should be 15
19
+
20
+ s.should.respond_to? :duration
21
+ s.should.respond_to? :length
22
+ s.should.respond_to? :started
23
+ s.should.respond_to? :title
24
+ s.should.respond_to? :track
25
+ s.should.respond_to? :votes
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'terminal_player'
3
+
4
+ describe Site do
5
+ it "bombs with no url" do
6
+ options = {cache: 999, cache_min: 12, url: ''}
7
+ lambda { Site.new(options, "di") }.should raise_error RuntimeError
8
+ end
9
+
10
+ it "sets up properly for di.fm" do
11
+ options = {cache: 999, cache_min: 12, url: 'http://www.di.fm/breaks.pls'}
12
+ s = Site.new(options, 'di')
13
+ s.name.should == 'di'
14
+ s.current_channel.should == 'breaks'
15
+ s.player.class.should be Mplayer
16
+ s.is_mplayer.should be true
17
+ end
18
+
19
+ it "should return appropriate types for spotify URIs" do
20
+ # use di because firing up spotify without username/password bombs
21
+ options = {cache: 999, cache_min: 12, url: 'http://www.di.fm/breaks.pls'}
22
+ s = Site.new(options, 'di')
23
+
24
+ output = s.send(:spotify_type, 'spotify:album:5PFwYpKSHE3Zab4YTrgyv2')
25
+ output.should == 'album'
26
+
27
+ output = s.send(:spotify_type, 'spotify:playlist:5PFwYpKSHE3Zab4YTrgyv2')
28
+ output.should == 'playlist'
29
+
30
+ output = s.send(:spotify_type, 'spotify:track:5PFwYpKSHE3Zab4YTrgyv2')
31
+ output.should == 'track'
32
+
33
+ output = s.send(:spotify_type, 'spotify:unknown:5PFwYpKSHE3Zab4YTrgyv2')
34
+ output.should == 'spotify:unknown:5PFwYpKSHE3Zab4YTrgyv2'
35
+ end
36
+ end
@@ -0,0 +1,138 @@
1
+ require 'spec_helper'
2
+ require 'terminal_player'
3
+
4
+ def capture_stdout(&block)
5
+ original_stdout = $stdout
6
+ $stdout = fake = StringIO.new
7
+ begin
8
+ yield
9
+ ensure
10
+ $stdout = original_stdout
11
+ end
12
+ fake.string
13
+ end
14
+
15
+ describe TerminalPlayer do
16
+ it "sets up properly for di.fm" do
17
+ options = { url: 'http://www.di.fm/' }
18
+ tp = TerminalPlayer.new(options)
19
+ s = tp.instance_variable_get(:@site)
20
+ s.class.should be DI
21
+ s.instance_variable_get(:@name).should == 'di-lo'
22
+ end
23
+
24
+ it "sets up properly for somafm.com" do
25
+ options = { url: 'http://somafm.com/' }
26
+ tp = TerminalPlayer.new(options)
27
+ tp.instance_variable_get(:@site).class.should be Soma
28
+ end
29
+
30
+ it "sets up properly for spotify" do
31
+ options = { url: 'spotify:blah:blah' }
32
+ tp = TerminalPlayer.new(options)
33
+ tp.instance_variable_get(:@site).class.should be Spotiphy
34
+ end
35
+
36
+ it "bombs with a bad site" do
37
+ options = { url: 'unknown' }
38
+ lambda { TerminalPlayer.new(options) }.should raise_error
39
+ end
40
+
41
+ it "lists di channels" do
42
+ options = { url: 'http://www.di.fm/channels.pls' }
43
+ output = capture_stdout do
44
+ lambda { TerminalPlayer.new(options) }.should raise_error SystemExit
45
+ end
46
+ output.should include 'ambient'
47
+ output.should include 'breaks'
48
+ output.should include 'vocaltrance'
49
+ end
50
+
51
+ it "lists soma channels" do
52
+ options = { url: 'http://somafm.com/channels.pls' }
53
+ output = capture_stdout do
54
+ lambda { TerminalPlayer.new(options) }.should raise_error SystemExit
55
+ end
56
+ output.should include 'doomed'
57
+ output.should include 'lush64'
58
+ output.should include 'u80s130'
59
+ end
60
+
61
+ it "displays songs" do
62
+ options = {
63
+ url: 'http://www.di.fm/premium_high/breaks.pls?abc123',
64
+ play_history_path: ''
65
+ }
66
+ tp = TerminalPlayer.new(options)
67
+ tp.instance_variable_get(:@site).class.should be DI
68
+ tp.instance_variable_get(:@site).instance_variable_get(:@name).should == 'di-hi'
69
+ tp.instance_variable_get(:@site).instance_variable_get(:@current_channel).should == 'breaks'
70
+
71
+ t = Time.now
72
+ output = capture_stdout do
73
+ songs = ['one', 'two', 'three']
74
+ tp.update(t, songs)
75
+ end
76
+ output.should include t.strftime("%H:%M:%S")
77
+ output.should include '[di-hi/breaks]'
78
+ output.should include 'three'
79
+ end
80
+
81
+ it "doesn't display duplicate songs" do
82
+ options = {
83
+ url: 'http://www.di.fm/premium_high/breaks.pls?abc123',
84
+ play_history_path: ''
85
+ }
86
+ tp = TerminalPlayer.new(options)
87
+ t = Time.now
88
+ songs = ['one', 'two', 'three']
89
+ output = capture_stdout do
90
+ tp.update(t, songs)
91
+ end
92
+ output.scan(/three/).length.should == 1
93
+
94
+ # add a dupe
95
+ songs << 'three'
96
+ output += capture_stdout do
97
+ tp.update(t, songs)
98
+ end
99
+ output.scan(/three/).length.should == 1
100
+ end
101
+
102
+ it "cleans song titles" do
103
+ options = { url: 'http://somafm.com/' }
104
+ tp = TerminalPlayer.new(options)
105
+ ab = 'ali baba and the forty theieves'
106
+ output = tp.send(:cleanup, ab)
107
+ output.should == ab.gsub(/ /, '+')
108
+ end
109
+
110
+ it "removes common crud in song titles" do
111
+ options = { url: 'http://somafm.com/' }
112
+ tp = TerminalPlayer.new(options)
113
+ # Feat.
114
+ ab = 'ali baba Feat. the forty theieves'
115
+ output = tp.send(:cleanup, ab)
116
+ output.should == 'ali+baba+the+forty+theieves'
117
+
118
+ # Many, but not all, non-word characters
119
+ ab = "?ali \tbaba , th\ne .forty thei^eves&*"
120
+ output = tp.send(:cleanup, ab)
121
+ output.should == 'ali+baba+the+.forty+theieves'
122
+ end
123
+
124
+ it "removes stuff in parenthesis from song titles" do
125
+ options = { url: 'http://somafm.com/' }
126
+ tp = TerminalPlayer.new(options)
127
+
128
+ # anything in parenthesis
129
+ ab = 'ali baba (original mix) - forty thieves'
130
+ output = tp.send(:cleanup, ab)
131
+ output.should == 'ali+baba+forty+thieves'
132
+
133
+ # ... even partially
134
+ ab = 'ali baba (ori'
135
+ output = tp.send(:cleanup, ab)
136
+ output.should == 'ali+baba'
137
+ end
138
+ end
@@ -0,0 +1,17 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
Binary file
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.authors = ['Shane Thomas']
3
+ gem.email = ['shane@devshane.com']
4
+ gem.homepage = 'https://github.com/devshane/terminal-player'
5
+
6
+ gem.summary = 'A minimalistic terminal-based player for di.fm, somafm.com, and Spotify.'
7
+ gem.description = 'Terminal player is a minimalistic terminal-based player for di.fm, somafm.com, and Spotify.'
8
+
9
+ gem.files = `git ls-files`.split($\)
10
+ gem.executables = ['terminal_player']
11
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
12
+
13
+ gem.name = 'terminal_player'
14
+ gem.version = '0.0.7'
15
+ gem.date = '2013-12-15'
16
+ gem.licenses = ['MIT']
17
+
18
+ gem.require_paths = ['lib']
19
+
20
+ gem.required_ruby_version = '>= 1.9.3'
21
+
22
+ gem.add_runtime_dependency('rake', '~> 10.1.0')
23
+ gem.add_runtime_dependency('rspec', '~> 2.14.1')
24
+ gem.add_runtime_dependency('spotify', '~> 12.5.3')
25
+ gem.add_runtime_dependency('plaything', '~> 1.1.1')
26
+ end