ektoplayer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +49 -0
  3. data/bin/ektoplayer +7 -0
  4. data/lib/ektoplayer.rb +10 -0
  5. data/lib/ektoplayer/application.rb +148 -0
  6. data/lib/ektoplayer/bindings.rb +230 -0
  7. data/lib/ektoplayer/browsepage.rb +138 -0
  8. data/lib/ektoplayer/client.rb +18 -0
  9. data/lib/ektoplayer/common.rb +91 -0
  10. data/lib/ektoplayer/config.rb +247 -0
  11. data/lib/ektoplayer/controllers/browser.rb +47 -0
  12. data/lib/ektoplayer/controllers/controller.rb +9 -0
  13. data/lib/ektoplayer/controllers/help.rb +21 -0
  14. data/lib/ektoplayer/controllers/info.rb +22 -0
  15. data/lib/ektoplayer/controllers/mainwindow.rb +40 -0
  16. data/lib/ektoplayer/controllers/playlist.rb +60 -0
  17. data/lib/ektoplayer/database.rb +199 -0
  18. data/lib/ektoplayer/events.rb +56 -0
  19. data/lib/ektoplayer/models/browser.rb +127 -0
  20. data/lib/ektoplayer/models/database.rb +49 -0
  21. data/lib/ektoplayer/models/model.rb +15 -0
  22. data/lib/ektoplayer/models/player.rb +28 -0
  23. data/lib/ektoplayer/models/playlist.rb +72 -0
  24. data/lib/ektoplayer/models/search.rb +42 -0
  25. data/lib/ektoplayer/models/trackloader.rb +17 -0
  26. data/lib/ektoplayer/mp3player.rb +151 -0
  27. data/lib/ektoplayer/operations/browser.rb +19 -0
  28. data/lib/ektoplayer/operations/operations.rb +26 -0
  29. data/lib/ektoplayer/operations/player.rb +11 -0
  30. data/lib/ektoplayer/operations/playlist.rb +67 -0
  31. data/lib/ektoplayer/theme.rb +102 -0
  32. data/lib/ektoplayer/trackloader.rb +146 -0
  33. data/lib/ektoplayer/ui.rb +404 -0
  34. data/lib/ektoplayer/ui/colors.rb +105 -0
  35. data/lib/ektoplayer/ui/widgets.rb +195 -0
  36. data/lib/ektoplayer/ui/widgets/container.rb +125 -0
  37. data/lib/ektoplayer/ui/widgets/labelwidget.rb +43 -0
  38. data/lib/ektoplayer/ui/widgets/listwidget.rb +332 -0
  39. data/lib/ektoplayer/ui/widgets/tabbedcontainer.rb +110 -0
  40. data/lib/ektoplayer/updater.rb +77 -0
  41. data/lib/ektoplayer/views/browser.rb +25 -0
  42. data/lib/ektoplayer/views/help.rb +46 -0
  43. data/lib/ektoplayer/views/info.rb +208 -0
  44. data/lib/ektoplayer/views/mainwindow.rb +64 -0
  45. data/lib/ektoplayer/views/playinginfo.rb +135 -0
  46. data/lib/ektoplayer/views/playlist.rb +39 -0
  47. data/lib/ektoplayer/views/progressbar.rb +51 -0
  48. data/lib/ektoplayer/views/splash.rb +99 -0
  49. data/lib/ektoplayer/views/trackrenderer.rb +137 -0
  50. data/lib/ektoplayer/views/volumemeter.rb +74 -0
  51. metadata +164 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6f7e3bffc50021abf9f3c8384db4c81952abb036
4
+ data.tar.gz: a7d7612d2175422b4e1f59d01e6c3992915bbb6c
5
+ SHA512:
6
+ metadata.gz: 788048c6464ae80c0d1d1ef2d0a85a307787ab8d8b7116cb4f2bf934223087238d5eb2af9ce48973899a248daee415aca40a23cf3a63c80d2554ab5a8fd7c8a7
7
+ data.tar.gz: 16bb67c65cd1b0b6c530d4f6e74f36b32d3a727e44125c8ba29c1b5e1d467362ec36d5b81ceb7a6ce5aa223d4cedd6accd8ea354b2f871d765fd581c1b82cd1a
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # Ektoplayer
2
+
3
+ Ektoplayer is a console audio player for [ektplazm.com](http://www.ektoplazm.com).
4
+
5
+ ![Screenshot 2017-03-16](http://pixelbanane.de/yafu/118231024/ekto1.png)
6
+ ![Screenshot 2017-03-16](http://pixelbanane.de/yafu/324630271/ekto2.png)
7
+
8
+ ## Requirements
9
+
10
+ * Ruby (1.9)
11
+ * Portaudio (19)
12
+ * Mpg123 (1.14)
13
+ * Sqlite3
14
+
15
+ ## Installation
16
+
17
+ Assuming you have Ruby/Rubygems installed, you need portaudio and mpg123 as
18
+ library to compile the native extensions.
19
+
20
+ ### Arch Linux
21
+
22
+ pacman -S portaudio mpg123 ncurses ruby sqlite
23
+ gem install ektoplayer
24
+
25
+ ### Debian / Ubuntu (not yet tested)
26
+
27
+ apt-get install portaudio19-dev libmpg123-dev libncurses-dev ruby1.9.1-dev sqlite-??
28
+ gem install ektoplayer
29
+
30
+ ## Features
31
+
32
+ * Listen to ektoplazm tracks
33
+ * Download whole albums
34
+ * Browse database by tags
35
+ * Vi keybindings (`hjkl`, `^d`, `^u`, `/`, `?`, `n`, `N`, `:`, ...)
36
+ * Mouse is supported
37
+ * Supports 256/16/mono colors
38
+ * Local sound file cache and download archive
39
+ * Highly configurable
40
+
41
+ ## Authors
42
+
43
+ * [Benjamin Abendroth](https://github.com/braph)
44
+
45
+ ## See also
46
+
47
+ * Ektoplayer was inspired by [Soundcloud2000](https://github.com/grobie/soundcloud2000) and [ncmpcpp](https://github.com/arybczak/ncmpcpp)
48
+ * It uses [Audite](https://github.com/georgi/audite) as playback engine and [Nokogiri](http://www.nokogiri.org/) for parsing HTML
49
+
data/bin/ektoplayer ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../lib', File.dirname(__FILE__))
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib)
5
+
6
+ require 'ektoplayer'
7
+ Ektoplayer::Ektoplayer.start
data/lib/ektoplayer.rb ADDED
@@ -0,0 +1,10 @@
1
+ require_relative 'ektoplayer/application'
2
+
3
+ module Ektoplayer
4
+ class Ektoplayer
5
+ def Ektoplayer.start
6
+ app = Application.new
7
+ app.run
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,148 @@
1
+ require 'fileutils'
2
+ require 'date'
3
+
4
+ { '.' => %w(config theme bindings client common ui),
5
+ 'views' => %w(mainwindow),
6
+ 'models' => %w(player browser playlist database trackloader),
7
+ 'operations' => %w(operations player browser playlist),
8
+ 'controllers' => %w(mainwindow browser playlist info help)
9
+ }.each { |d,files| files.each { |f| require_relative "#{d}/#{f}" } }
10
+
11
+ module Ektoplayer
12
+ class Application
13
+ VERSION = '0.1'.freeze
14
+ GITHUB_URL = 'https://github.com/braph/ektoplayer'.freeze
15
+ EKTOPLAZM_URL = 'http://www.ektoplazm.com'.freeze
16
+
17
+ CONFIG_DIR = File.join(Dir.home, '.config', 'ektoplayer').freeze
18
+ CONFIG_FILE = File.join(CONFIG_DIR, 'ektoplayer.rc').freeze
19
+
20
+ def self.log(from, *msgs)
21
+ func = caller[0][/`.*'/][1..-2]
22
+ from = from.class unless from.is_a?String
23
+ $stderr.puts("#{DateTime.now.rfc3339} #{from}.#{func}: " + msgs.join(' '))
24
+
25
+ if e = msgs.select { |m| m.kind_of?Exception }[0]
26
+ $stderr.puts "#{e.backtrace.first}: #{e.message} (#{e.class})", e.backtrace.drop(1).map{|s| "\t#{s}"}
27
+ end
28
+ $stderr.flush
29
+ end
30
+
31
+ def self.open_log(file)
32
+ $stderr.reopen(file, ?a)
33
+ $stderr.sync=(true)
34
+ end
35
+
36
+ def run
37
+ #Thread.abort_on_exception=(true)
38
+
39
+ # make each configuration object globally accessible as a singleton
40
+ [Config, Bindings, Theme].each { |c| Common::mksingleton(c) }
41
+
42
+ if File.file? Config::CONFIG_FILE
43
+ Config.parse(Config::CONFIG_FILE, Bindings, Theme) rescue (
44
+ fail "Config: #{$!}"
45
+ )
46
+ end
47
+
48
+ FileUtils::mkdir_p Config::CONFIG_DIR rescue (
49
+ fail "Could not create config dir: #{$!}"
50
+ )
51
+
52
+ Application.open_log(Config[:log_file])
53
+
54
+ if Config[:use_cache]
55
+ unless File.directory? Config[:cache_dir]
56
+ FileUtils::mkdir Config[:cache_dir] rescue (
57
+ fail "Could not create cache dir: #{$!}"
58
+ )
59
+ end
60
+ end
61
+
62
+ [:temp_dir, :download_dir, :archive_dir].each do |key|
63
+ unless File.directory? Config[key]
64
+ FileUtils::mkdir Config[key] rescue (
65
+ fail "Could not create #{key}: #{$!}"
66
+ )
67
+ end
68
+ end
69
+
70
+ UI::Canvas.run do
71
+ if Config[:use_colors] == :auto
72
+ Theme.use_colors(ENV['TERM'] =~ /256/ ? 256 : 8)
73
+ else
74
+ Theme.use_colors(Config[:use_colors])
75
+ end
76
+
77
+ client = Client.new
78
+
79
+ # ... models ...
80
+ player = Models::Player.new(client)
81
+ browser = Models::Browser.new(client)
82
+ playlist = Models::Playlist.new
83
+ database = Models::Database.new(client)
84
+ trackloader = Models::Trackloader.new(client)
85
+
86
+ # ... operations ...
87
+ operations = Operations::Operations.new
88
+ operations.register(:quit, &method(:exit))
89
+ operations.register(:reload, &browser.method(:reload))
90
+ operations.register(:update, &database.method(:update))
91
+ operations.register(:refresh) { UI::Canvas.on_winch; UI::Canvas.update_screen(true) }
92
+ Operations::Player.new(operations, player)
93
+ Operations::Browser.new(operations, browser, playlist)
94
+ Operations::Playlist.new(operations, playlist, player, trackloader)
95
+
96
+ # ... views ...
97
+ main_w = UI::Canvas.sub(Views::MainWindow)
98
+
99
+ # next operations may take some time, espacially the ones
100
+ # using the database (browser), so we put this inside a thread
101
+ Thread.new do
102
+ # ... controllers ...
103
+ view_ops = Operations::Operations.new
104
+ Controllers::MainWindow.new(main_w, view_ops)
105
+ Controllers::Browser.new(main_w.browser, browser, view_ops, operations)
106
+ Controllers::Playlist.new(main_w.playlist, playlist, view_ops, operations)
107
+ Controllers::Help.new(main_w.help, view_ops)
108
+ Controllers::Info.new(main_w.info, playlist, trackloader, database, view_ops)
109
+ main_w.progressbar.attach(player)
110
+ main_w.volumemeter.attach(player)
111
+ main_w.playinginfo.attach(playlist, player)
112
+
113
+ # ... events ...
114
+ database.events.on(:update_finished, &browser.method(:reload ))
115
+ player.events.on(:stop) do |reason|
116
+ operations.send(:'playlist.play_next') if reason == :track_completed
117
+ end
118
+
119
+ # ... bindings ...
120
+ Bindings.bind_view(:global, main_w, view_ops, operations)
121
+ %w(splash playlist browser info help).each do |w|
122
+ Bindings.bind_view(w, main_w.send(w), view_ops, operations)
123
+ end
124
+
125
+ player.stop
126
+
127
+ # If database is empty, start an initial update
128
+ if browser.tracks(0).size < 1
129
+ operations.send(:update)
130
+ elsif (c = Config[:small_update_pages]) > 0
131
+ operations.send(:update, pages: c)
132
+ end
133
+
134
+ if (n = Config[:playlist_load_newest]) > 0
135
+ r = client.database.select(order_by: 'date', limit: n)
136
+ playlist.add(*r)
137
+ end
138
+ end
139
+
140
+ UI::Canvas.update_screen
141
+ UI::Input.start_loop
142
+ end
143
+ rescue
144
+ puts "Error: #{e}"
145
+ Application.log(self, $!)
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,230 @@
1
+ require 'set'
2
+ require 'curses'
3
+
4
+ module Ektoplayer
5
+ # Keybinding storage.
6
+ #
7
+ # Provides access for changing keybinds (with collision detection):
8
+ # bind(), unbind()
9
+ #
10
+ # Provides binding these keys to widgets
11
+ # bind_section_to_widget(), bind_to_widget()
12
+ #
13
+ class Bindings
14
+ attr_reader :bindings, :commands
15
+
16
+ def register(command, description)
17
+ @commands[command.to_sym] = description.freeze
18
+ end
19
+ alias :reg :register
20
+
21
+ def initialize
22
+ @commands = {}
23
+
24
+ reg 'quit', 'Quit the program'
25
+ reg 'refresh', 'Refresh the screen'
26
+ reg 'reload', 'Apply database changes to browser'
27
+ reg 'update', 'Start a database update'
28
+
29
+ reg 'player.stop', 'Stop playing'
30
+ reg 'player.toggle', 'Toggle play/pause'
31
+ reg 'player.forward', 'Seek forward'
32
+ reg 'player.backward', 'Seek backward'
33
+
34
+ reg 'tabs.next', 'Select next tab'
35
+ reg 'tabs.prev', 'Select previous tab'
36
+
37
+ reg 'browser.enter', 'Enter selected directory'
38
+ reg 'browser.add_to_playlist', 'Add tracks under cursor to playlist'
39
+ reg 'browser.back', 'Change to parent directory'
40
+
41
+ reg 'playinginfo.toggle', 'Toggle playinginfo visibility'
42
+ reg 'progressbar.toggle', 'Toggle progressbar visibility'
43
+ reg 'volumemeter.toggle', 'Toggle volumemeter visibility'
44
+
45
+ reg 'playlist.goto_current', 'Go to current playing track'
46
+ reg 'playlist.clear', 'Delete all items in playlist'
47
+ reg 'playlist.delete', 'Delete selected track from playlist'
48
+ reg 'playlist.download_album', 'Download album archive for selected track'
49
+ reg 'playlist.play', 'Play selected track'
50
+ reg 'playlist.reload', 'Force redownload of selected track'
51
+ reg 'playlist.play_next', 'Play next track in playlist'
52
+ reg 'playlist.play_prev', 'Play previous track in playlist'
53
+
54
+ %w(info help browser playlist splash).each do |w|
55
+ reg("#{w}.show", "Show #{w}")
56
+ end
57
+
58
+ { up: 'Move cursor up', down: 'Move cursor down',
59
+ page_up: 'Scroll page up', page_down: 'Scroll page down',
60
+ top: 'Move to top', bottom: 'Move to bottom'
61
+ }.each do |cmd, desc|
62
+ %w(browser info help playlist).each { |w| reg("#{w}.#{cmd}", desc) }
63
+ end
64
+
65
+ { search_up: 'Start search upwards', search_next: 'Goto next search result',
66
+ search_down: 'Start search downwards', search_prev: 'Goto previous search result'
67
+ }.each do |cmd, desc|
68
+ %w(browser playlist).each { |w| reg("#{w}.#{cmd}", desc) }
69
+ end
70
+
71
+ @bindings = {
72
+ global: {
73
+ 'splash.show': [?`, ?^ ],
74
+ 'playlist.show': [?1 ],
75
+ 'browser.show': [?2 ],
76
+ 'info.show': [?3 ],
77
+ 'help.show': [?4 ],
78
+
79
+ 'playinginfo.toggle': [ Curses::KEY_F2 ],
80
+ 'progressbar.toggle': [ Curses::KEY_F3 ],
81
+ 'volumemeter.toggle': [ Curses::KEY_F4 ],
82
+
83
+ 'player.forward': [?f, Curses::KEY_RIGHT ],
84
+ 'player.backward': [?b, Curses::KEY_LEFT ],
85
+ 'player.stop': [?s ],
86
+ 'player.toggle': [?p ],
87
+
88
+ 'playlist.play_next': [?> ],
89
+ 'playlist.play_prev': [?< ],
90
+
91
+ 'tabs.next': [?l ],
92
+ 'tabs.prev': [?h ],
93
+
94
+ quit: [?q ],
95
+ refresh: ['^l' ],
96
+ reload: ['^r' ],
97
+ update: [?U ]},
98
+ playlist: {
99
+ # movement
100
+ 'playlist.top': [?g, Curses::KEY_HOME ],
101
+ 'playlist.bottom': [?G, Curses::KEY_END ],
102
+ 'playlist.up': [?k, Curses::KEY_UP ],
103
+ 'playlist.down': [?j, Curses::KEY_DOWN ],
104
+ 'playlist.page_down': ['^d', Curses::KEY_NPAGE ],
105
+ 'playlist.page_up': ['^u', Curses::KEY_PPAGE ],
106
+ # search
107
+ 'playlist.search_next': [?n ],
108
+ 'playlist.search_prev': [?N ],
109
+ 'playlist.search_up': [?? ],
110
+ 'playlist.search_down': [?/ ],
111
+ # playlist
112
+ 'playlist.play': [ Curses::KEY_ENTER ],
113
+ 'playlist.download_album': [?$ ],
114
+ 'playlist.reload': [?r ],
115
+ 'playlist.goto_current': [?o ],
116
+ 'playlist.clear': [?c ],
117
+ 'playlist.delete': [?d ],
118
+ # other
119
+ 'player.toggle': [' ' ]},
120
+ browser: {
121
+ # movement
122
+ 'browser.top': [?g, Curses::KEY_HOME ],
123
+ 'browser.bottom': [?G, Curses::KEY_END ],
124
+ 'browser.up': [?k, Curses::KEY_UP ],
125
+ 'browser.down': [?j, Curses::KEY_DOWN ],
126
+ 'browser.page_up': ['^u', Curses::KEY_PPAGE ],
127
+ 'browser.page_down': ['^d', Curses::KEY_NPAGE ],
128
+ # search
129
+ 'browser.search_next': [?n ],
130
+ 'browser.search_prev': [?N ],
131
+ 'browser.search_up': [?? ],
132
+ 'browser.search_down': [?/ ],
133
+ # browser
134
+ 'browser.add_to_playlist': [' ', ?a ],
135
+ 'browser.enter': [?E, Curses::KEY_ENTER ],
136
+ 'browser.back': [?b, Curses::KEY_BACKSPACE ]},
137
+ help: {
138
+ 'help.top': [?g, Curses::KEY_HOME ],
139
+ 'help.bottom': [?G, Curses::KEY_END ],
140
+ 'help.up': [?k, Curses::KEY_UP ],
141
+ 'help.down': [?j, Curses::KEY_DOWN ],
142
+ 'help.page_up': ['^u', Curses::KEY_PPAGE ],
143
+ 'help.page_down': ['^d', Curses::KEY_NPAGE ]},
144
+ info: {
145
+ 'info.top': [?g, Curses::KEY_HOME ],
146
+ 'info.bottom': [?G, Curses::KEY_END ],
147
+ 'info.up': [?k, Curses::KEY_UP ],
148
+ 'info.down': [?j, Curses::KEY_DOWN ],
149
+ 'info.page_up': ['^u', Curses::KEY_PPAGE ],
150
+ 'info.page_down': ['^d', Curses::KEY_NPAGE ]},
151
+ splash: {}
152
+ }
153
+
154
+ @bindings.default_proc = proc { |h,k| fail "Unknown widget #{k}" }
155
+ @bindings.each do |widget, hash|
156
+ hash.default_proc = proc { |h,k| h[k] = {} }
157
+ hash.values.each do |keys|
158
+ keys.map! { |key| parse_key(key) }
159
+ end
160
+ end
161
+ end
162
+
163
+ def keyname(key)
164
+ return 'SPACE' if key.to_s == ' '
165
+ return key.to_s if key.is_a? Symbol
166
+
167
+ name = Curses.keyname(key)
168
+ if name.start_with? 'KEY_'
169
+ name.sub('KEY_', '').sub(/\((\d+)\)/, '\1')
170
+ else
171
+ name
172
+ end
173
+ end
174
+
175
+ def parse_key(key)
176
+ if key.is_a? Integer
177
+ key
178
+ elsif key.size == 1
179
+ key.to_sym
180
+ elsif key.size == 2 and key.start_with?(?^)
181
+ Curses.const_get("KEY_CTRL_#{key[1].upcase}")
182
+ else
183
+ key = key.upcase.tr(?-, ?_)
184
+ key = "KEY_#{key}" unless key.start_with?('KEY_')
185
+ Curses.const_get(key)
186
+ end
187
+ rescue NameError
188
+ fail "Unknown key: #{key}"
189
+ end
190
+
191
+ def bind(widget, key, command)
192
+ widget, command = widget.to_sym, command.to_sym
193
+ fail "Unknown command #{command}" unless COMMANDS.include? command
194
+
195
+ @bindings[widget][command].delete parse_key(key)
196
+ @bindings[widget][command] << parse_key(key)
197
+ check_collisions
198
+ end
199
+
200
+ def unbind(widget, key)
201
+ @bindings[widget.to_sym].each do |command, keys|
202
+ keys.delete( (parsed_key ||= parse_key(k)) )
203
+ end
204
+ end
205
+
206
+ def bind_view(section, view, view_operations, operations)
207
+ @bindings[section.to_sym].each do |command, keys|
208
+ keys.each do |key|
209
+ meth = view_operations.method(command) rescue operations.method(command)
210
+ view.keys.on(key, &meth)
211
+ end
212
+ end
213
+ end
214
+
215
+ private def check_collisions
216
+ global_keys = @bindings[:global].values.flatten
217
+ global_keys.each do |k|
218
+ fail "Double binding in 'global': #{k}" if global_keys.count(k) > 1
219
+ end
220
+
221
+ @bindings.each_pair do |widget, commands|
222
+ widget_keys = commands.values.flatten
223
+ widget_keys.each do |k|
224
+ fail "Double binding in '#{widget}': #{k}" if widget_keys.count(k) > 1
225
+ fail "Double binding in 'global/#{widget}': #{k}" if global_keys.include? k
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end