ektoplayer 0.1.2 → 0.1.3

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
  SHA1:
3
- metadata.gz: 44752ed0ce943af8aeba6ed7af645b94e40b9a8a
4
- data.tar.gz: b5796fa336978ab5c8c72fb3a6c732084aee5575
3
+ metadata.gz: 55540b5adfb0dec28702908ee968d434b802221a
4
+ data.tar.gz: 5f11aef936c061803e546ea22a431193fa106685
5
5
  SHA512:
6
- metadata.gz: 268d0f386b0db1fb392c47ed61c3e1d7e16599776dd7d03e001b769ccd9e6b5cdd982d3fcb0d68796907e489708034765876c21d76c8df77a1f7496ebd201911
7
- data.tar.gz: 9f30c21d456766c06d9e9c9d23f9a0f6a3cf4d773430b0f45ba521784fdf577eee89d85bb543f5d9bc4ee4f17bf0402387f3887ae98d40b4f0ec9a96d9e7781e
6
+ metadata.gz: '098d4f6de14917ac00d6321729827ca49469628b26d9a1b16ba3054660886893f6f5e7ef1837cdc3ebacae2703b8b56e835d6ecec5243ec5ee45bccc5ab8dff8'
7
+ data.tar.gz: 1bf895998e866f025d9b6d13775c0d32a9352c21a0aaa7fa6d4743885775444f43a741f86fc1a8288d8251d3ab35e7883aac23eb92471ab87df3e945c0d9ba63
data/README.md CHANGED
@@ -1,9 +1,25 @@
1
1
  # Ektoplayer
2
2
 
3
- Ektoplayer is a console audio player for [ektoplazm.com](http://www.ektoplazm.com).
3
+ Ektoplayer is a commandline client for [ektoplazm.com](http://www.ektoplazm.com), a website where you can listen and download freely licensed psytrance, techno and downtempo music.
4
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)
5
+ It allows you to
6
+ * Search for tracks by tags (artist, album, style, ...)
7
+ * Play tracks located at ektoplazm.com
8
+ * Display information about albums
9
+ * Download a whole albums as mp3
10
+
11
+ ## Features
12
+
13
+ * Mouse support
14
+ * Vi-like keybindings (`hjkl`, `^d`, `^u`, `/`, `?`, `n`, `N`, ...)
15
+ * Up to 256 colors are supported
16
+ * Local sound file cache
17
+
18
+ ## Screenshots
19
+
20
+ ![Screenshot 2017-03-18](http://pixelbanane.de/yafu/1384751165/ekto1.gif)
21
+ ![Screenshot 2017-03-18](http://pixelbanane.de/yafu/3868182865/ekto2.gif)
22
+ ![Screenshot 2017-03-18](http://pixelbanane.de/yafu/2446075869/ekto3.gif)
7
23
 
8
24
  ## Requirements
9
25
 
@@ -28,17 +44,6 @@ library to compile the native extensions.
28
44
  apt-get install ruby ruby-dev portaudio19-dev libmpg123-dev sqlite3 libsqlite3-dev libncurses-dev libz1g-dev build-essential
29
45
  gem install ektoplayer
30
46
 
31
- ## Features
32
-
33
- * Listen to ektoplazm tracks
34
- * Download whole albums
35
- * Browse database by tags
36
- * Vi keybindings (`hjkl`, `^d`, `^u`, `/`, `?`, `n`, `N`, ...)
37
- * Mouse is supported
38
- * Supports 256/16/mono colors
39
- * Local sound file cache and download archive
40
- * Highly configurable
41
-
42
47
  ## Authors
43
48
 
44
49
  * [Benjamin Abendroth](https://github.com/braph)
@@ -47,4 +52,3 @@ library to compile the native extensions.
47
52
 
48
53
  * Ektoplayer was inspired by [Soundcloud2000](https://github.com/grobie/soundcloud2000) and [ncmpcpp](https://github.com/arybczak/ncmpcpp)
49
54
  * It uses [Audite](https://github.com/georgi/audite) as playback engine and [Nokogiri](http://www.nokogiri.org/) for parsing HTML
50
-
@@ -10,7 +10,7 @@ require 'date'
10
10
 
11
11
  module Ektoplayer
12
12
  class Application
13
- VERSION = '0.1.2'.freeze
13
+ VERSION = '0.1.4'.freeze
14
14
  GITHUB_URL = 'https://github.com/braph/ektoplayer'.freeze
15
15
  EKTOPLAZM_URL = 'http://www.ektoplazm.com'.freeze
16
16
 
@@ -35,6 +35,7 @@ module Ektoplayer
35
35
 
36
36
  def run
37
37
  #Thread.abort_on_exception=(true)
38
+ Thread.report_on_exception=(true) rescue nil
38
39
 
39
40
  # make each configuration object globally accessible as a singleton
40
41
  [Config, Bindings, Theme].each { |c| Common::mksingleton(c) }
@@ -99,13 +100,15 @@ module Ektoplayer
99
100
  # next operations may take some time, espacially the ones
100
101
  # using the database (browser), so we put this inside a thread
101
102
  Thread.new do
103
+ begin
104
+
102
105
  # ... controllers ...
103
106
  view_ops = Operations::Operations.new
104
107
  Controllers::MainWindow.new(main_w, view_ops)
105
108
  Controllers::Browser.new(main_w.browser, browser, view_ops, operations)
106
109
  Controllers::Playlist.new(main_w.playlist, playlist, view_ops, operations)
107
110
  Controllers::Help.new(main_w.help, view_ops)
108
- Controllers::Info.new(main_w.info, playlist, trackloader, database, view_ops)
111
+ Controllers::Info.new(main_w.info, player, playlist, trackloader, database, view_ops)
109
112
  main_w.progressbar.attach(player)
110
113
  main_w.volumemeter.attach(player)
111
114
  main_w.playinginfo.attach(playlist, player)
@@ -115,6 +118,20 @@ module Ektoplayer
115
118
  player.events.on(:stop) do |reason|
116
119
  operations.send(:'playlist.play_next') if reason == :track_completed
117
120
  end
121
+
122
+ if Config[:prefetch]
123
+ trackloader_mutex = Mutex.new
124
+ player.events.on(:position_change) do
125
+ Thread.new do
126
+ if player.length > 30 and player.position_percent > 0.8
127
+ trackloader_mutex.synchronize do
128
+ trackloader.get_track_file(playlist[playlist.get_next_pos]['url'])
129
+ sleep 5
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
118
135
 
119
136
  # ... bindings ...
120
137
  Bindings.bind_view(:global, main_w, view_ops, operations)
@@ -135,6 +152,10 @@ module Ektoplayer
135
152
  r = client.database.select(order_by: 'date', limit: n)
136
153
  playlist.add(*r)
137
154
  end
155
+
156
+ rescue
157
+ Application.log(self, $!)
158
+ end
138
159
  end
139
160
 
140
161
  UI::Canvas.update_screen
@@ -41,6 +41,7 @@ module Ektoplayer
41
41
  reg 'playinginfo.toggle', 'Toggle playinginfo visibility'
42
42
  reg 'progressbar.toggle', 'Toggle progressbar visibility'
43
43
  reg 'volumemeter.toggle', 'Toggle volumemeter visibility'
44
+ reg 'tabbar.toggle', 'Toggle tabbar visibility'
44
45
 
45
46
  reg 'playlist.goto_current', 'Go to current playing track'
46
47
  reg 'playlist.clear', 'Delete all items in playlist'
@@ -78,7 +79,8 @@ module Ektoplayer
78
79
 
79
80
  :'playinginfo.toggle' => [ Curses::KEY_F2 ],
80
81
  :'progressbar.toggle' => [ Curses::KEY_F3 ],
81
- :'volumemeter.toggle' => [ Curses::KEY_F4 ],
82
+ :'tabbar.toggle' => [ Curses::KEY_F4 ],
83
+ :'volumemeter.toggle' => [ Curses::KEY_F5 ],
82
84
 
83
85
  :'player.forward' => [?f, Curses::KEY_RIGHT ],
84
86
  :'player.backward' => [?b, Curses::KEY_LEFT ],
@@ -122,6 +122,9 @@ module Ektoplayer
122
122
  reg :use_cache,
123
123
  'Enable/disable local mp3 cache', true
124
124
 
125
+ reg :prefetch,
126
+ 'Enable prefetching next track do be played', true
127
+
125
128
  reg :small_update_pages,
126
129
  'How many pages should be fetched after start', 5
127
130
 
@@ -177,8 +180,8 @@ module Ektoplayer
177
180
  'Format of second line in playinginfo', DEFAULT_PLAYINGINFO_FORMAT2,
178
181
  ColumnFormat.method(:parse_simple_format)
179
182
 
180
- # - Tabs
181
- reg 'tabs.show_tabbar',
183
+ # - Tabbar
184
+ reg 'tabbar.display',
182
185
  'Enable/disable tabbar', true
183
186
 
184
187
  reg 'tabs.widgets', 'Specify widget order of tabbar (left to right)',
@@ -186,7 +189,7 @@ module Ektoplayer
186
189
  lambda { |v| v.split(/\s*,\s*/).map(&:to_sym) }
187
190
 
188
191
  reg 'main.widgets', 'Specify widgets to show (up to down)',
189
- 'playinginfo,progressbar,tabs,volumemeter',
192
+ 'playinginfo,progressbar,tabbar,windows,volumemeter',
190
193
  lambda { |v| v.split(/\s*,\s*/).map(&:to_sym) }
191
194
  end
192
195
 
@@ -5,9 +5,9 @@ require_relative 'controller'
5
5
  module Ektoplayer
6
6
  module Controllers
7
7
  class Info < Controller
8
- def initialize(view, playlist, trackloader, database, view_operations)
8
+ def initialize(view, player, playlist, trackloader, database, view_operations)
9
9
  super(view)
10
- view.attach(playlist, trackloader, database)
10
+ view.attach(player, playlist, trackloader, database)
11
11
  register = view_operations.with_register('info.')
12
12
  %w(up down page_up page_down top bottom).
13
13
  each { |op| register.(op, &view.method(op)) }
@@ -6,13 +6,20 @@ module Ektoplayer
6
6
  def initialize(view, view_operations)
7
7
  super(view)
8
8
  ops = view_operations
9
- ops.reg('splash.show') { view.tabs.selected=(view.splash) }
10
- ops.reg('playlist.show') { view.tabs.selected=(view.playlist) }
11
- ops.reg('browser.show') { view.tabs.selected=(view.browser) }
12
- ops.reg('info.show') { view.tabs.selected=(view.info) }
13
- ops.reg('help.show') { view.tabs.selected=(view.help) }
14
- ops.reg('tabs.next') { view.tabs.next }
15
- ops.reg('tabs.prev') { view.tabs.prev }
9
+ ops.reg('splash.show') { view.windows.selected=(view.splash) }
10
+ ops.reg('playlist.show') { view.windows.selected=(view.playlist) }
11
+ ops.reg('browser.show') { view.windows.selected=(view.browser) }
12
+ ops.reg('info.show') { view.windows.selected=(view.info) }
13
+ ops.reg('help.show') { view.windows.selected=(view.help) }
14
+ ops.reg('tabs.next') { view.windows.select_next }
15
+ ops.reg('tabs.prev') { view.windows.select_prev }
16
+
17
+ ops.reg('tabbar.toggle') do
18
+ view.with_lock do
19
+ view.tabbar.visible=(!view.tabbar.visible?)
20
+ view.want_layout
21
+ end
22
+ end
16
23
 
17
24
  ops.reg('playinginfo.toggle') do
18
25
  view.with_lock do
@@ -34,6 +41,14 @@ module Ektoplayer
34
41
  view.want_layout
35
42
  end
36
43
  end
44
+
45
+ view.tabbar.events.on(:tab_clicked) do |index|
46
+ view.windows.selected_index=(index)
47
+ end
48
+
49
+ view.windows.events.on(:changed) do |index|
50
+ view.tabbar.selected=(index)
51
+ end
37
52
  end
38
53
  end
39
54
  end
@@ -117,6 +117,14 @@ module Ektoplayer
117
117
  each { |t| @db.execute("DROP TABLE IF EXISTS #{t}") }
118
118
  end
119
119
 
120
+ def transaction
121
+ @db.transaction rescue Application.log(self, $!)
122
+ end
123
+
124
+ def commit
125
+ @db.commit rescue Application.log(self, $!)
126
+ end
127
+
120
128
  def insert_into(table, hash, mode: :insert)
121
129
  cols = ?( + (hash.keys * ?,) + ?)
122
130
  values = ?( + (([??] * hash.size) * ?,) + ?)
@@ -16,11 +16,13 @@ class Events
16
16
  # Disables auto creation of non existent events
17
17
  def no_auto_create
18
18
  @map.default_proc = proc { |h,k| fail KeyError, "Unknown event: #{k}" }
19
+ self
19
20
  end
20
21
 
21
22
  # Enables auto creation of non existent events
22
23
  def auto_create
23
24
  @map.default_proc = proc { |h,k| h[k] = [] }
25
+ self
24
26
  end
25
27
 
26
28
  # Registers a new event
@@ -52,5 +54,7 @@ class Events
52
54
  if @map.key?(event)
53
55
  @map[event].each { |callback| callback.call(*args) }
54
56
  end
57
+ rescue
58
+ Ektoplayer::Application.log(self, 'event hook failed', $!)
55
59
  end
56
60
  end
@@ -8,7 +8,8 @@ module Ektoplayer
8
8
  @current = 0
9
9
  @theme = {
10
10
  0 => { default: [-1, -1].freeze,
11
- :'url' => [-1, -1, :underline ].freeze},
11
+ :'url' => [-1, -1, :underline ].freeze,
12
+ :'tabbar.selected' => [-1, -1, :bold ].freeze},
12
13
  8 => { default: [-1, -1].freeze,
13
14
  :'url' => [:magenta, -1, :underline].freeze,
14
15
 
@@ -26,8 +27,8 @@ module Ektoplayer
26
27
  :'volumemeter.level' => [:magenta ].freeze,
27
28
  :'volumemeter.rest' => [:black ].freeze,
28
29
 
29
- :'tabs' => [:none ].freeze,
30
- :'tab_selected' => [:blue ].freeze,
30
+ :'tabbar.selected' => [:blue ].freeze,
31
+ :'tabbar.unselected' => [:none ].freeze,
31
32
 
32
33
  :'list.item_even' => [:blue ].freeze,
33
34
  :'list.item_odd' => [:blue ].freeze,
@@ -56,8 +57,8 @@ module Ektoplayer
56
57
  :'volumemeter.level' => [:magenta ].freeze,
57
58
  :'volumemeter.rest' => [236 ].freeze,
58
59
 
59
- :'tabs' => [250 ].freeze,
60
- :'tab_selected' => [75 ].freeze,
60
+ :'tabbar.selected' => [75 ].freeze,
61
+ :'tabbar.unselected' => [250 ].freeze,
61
62
 
62
63
  :'list.item_even' => [:blue ].freeze,
63
64
  :'list.item_odd' => [25 ].freeze,
@@ -77,10 +78,10 @@ module Ektoplayer
77
78
  def color_256(*args) color(*args, theme: 256) end
78
79
 
79
80
  def get(theme_def) UI::Colors.get(theme_def) end
80
- alias :[] :get
81
+ def [](theme_def) UI::Colors.get(theme_def) end
81
82
 
82
83
  def use_colors(colors)
83
- fail ArgumentError unless @theme[colors]
84
+ fail ArgumentError, 'unknown theme' unless @theme[colors]
84
85
  @current = colors
85
86
 
86
87
  UI::Colors.reset
@@ -55,6 +55,8 @@ module Ektoplayer
55
55
  end
56
56
 
57
57
  @downloads << dl.start!
58
+ rescue
59
+ Application.log(self, $!)
58
60
  end
59
61
 
60
62
  def get_track_file(url, reload: false)
@@ -97,6 +99,8 @@ module Ektoplayer
97
99
  @downloads << dl.start!
98
100
 
99
101
  return temp_file
102
+ rescue
103
+ Application.log(self, $!)
100
104
  end
101
105
  end
102
106
 
@@ -113,6 +117,8 @@ module Ektoplayer
113
117
  end
114
118
 
115
119
  def start!
120
+ Application.log(self, 'starting download: ', @url)
121
+
116
122
  Thread.new do
117
123
  begin
118
124
  http = Net::HTTP.new(@url.host, @url.port)
@@ -139,7 +145,7 @@ module Ektoplayer
139
145
  end
140
146
 
141
147
  sleep 0.1 while @total.nil?
142
- sleep 0.1
148
+ sleep 0.2
143
149
  self
144
150
  end
145
151
  end
@@ -6,6 +6,7 @@ module UI
6
6
 
7
7
  def initialize(widgets: [], **opts)
8
8
  super(**opts)
9
+ events.register(:changed)
9
10
  @selected, @selected_index, @widgets = nil, nil, widgets
10
11
  end
11
12
 
@@ -24,6 +25,7 @@ module UI
24
25
  end
25
26
 
26
27
  @selected_index = index
28
+ trigger(@events, :changed, @selected_index)
27
29
  want_layout
28
30
  end
29
31
  end
@@ -39,6 +41,7 @@ module UI
39
41
  end
40
42
 
41
43
  @selected = widget
44
+ trigger(@events, :changed, @selected_index)
42
45
  want_layout
43
46
  end
44
47
  end
@@ -70,19 +73,19 @@ module UI
70
73
 
71
74
  def draw; visible_widgets.each(&:draw) end
72
75
  def refresh; visible_widgets.each(&:refresh) end
73
- def layout; visible_widgets.each(&:layout) end
76
+ def layout; @widgets.each(&:layout) end
74
77
 
75
78
  def on_key_press(key)
76
79
  @selected.key_press(key) if @selected
77
80
  super(key)
78
81
  end
79
82
 
80
- def next
83
+ def select_next
81
84
  return unless @selected
82
85
  self.selected_index=((@selected_index + 1) % @widgets.size)
83
86
  end
84
87
 
85
- def prev
88
+ def select_prev
86
89
  return unless @selected
87
90
  return self.selected_index=(@widgets.size - 1) if @selected_index == 0
88
91
  self.selected_index=(@selected_index - 1)
@@ -122,4 +125,35 @@ module UI
122
125
  super
123
126
  end
124
127
  end
128
+
129
+ class SwitchContainer < GenericContainer
130
+ def layout
131
+ @widgets.each do |widget|
132
+ widget.with_lock do
133
+ widget.size=(@size)
134
+ widget.pos=(@pos)
135
+ end
136
+ end
137
+
138
+ super
139
+ end
140
+
141
+ def selected=(widget)
142
+ with_lock do
143
+ (@selected.invisible!) if @selected
144
+ super(widget)
145
+ (@selected.visible!) if @selected
146
+ want_layout
147
+ end
148
+ end
149
+
150
+ def selected_index=(index)
151
+ with_lock do
152
+ (@selected.invisible!) if @selected
153
+ super(index)
154
+ (@selected.visible!) if @selected
155
+ want_layout
156
+ end
157
+ end
158
+ end
125
159
  end
@@ -38,6 +38,8 @@ module UI
38
38
  height: @pad[:top] + @pad[:bottom] + 1 + @text.count(?\n),
39
39
  width: @pad[:left] + @pad[:right] + @text.split(?\n).max.size
40
40
  ))
41
+
42
+ self
41
43
  end
42
44
  end
43
45
  end
@@ -3,8 +3,8 @@ module UI
3
3
  WANT_REFRESH, WANT_REDRAW, WANT_LAYOUT = 1, 2, 4
4
4
 
5
5
  attr_reader :pos, :size
6
+ def events; @events ||= Events.new.no_auto_create end
6
7
  def keys; @keys ||= Events.new end
7
- def events; @events ||= Events.new end
8
8
  def mouse; @mouse ||= MouseEvents.new end
9
9
  def mouse_section; @mouse_section ||= MouseSectionEvents.new end
10
10
 
@@ -184,6 +184,32 @@ module UI
184
184
  self.pad_minrow=(new_minrow)
185
185
  end
186
186
 
187
+ def with_mouse_section_event
188
+ start_cursor = @win.cursor; yield
189
+
190
+ start_pos = UI::Point.new(
191
+ y: [start_cursor.y, @win.cursor.y].min,
192
+ x: [start_cursor.x, @win.cursor.x].min,
193
+ )
194
+ stop_pos = UI::Point.new(
195
+ y: [start_cursor.y, @win.cursor.y].max,
196
+ x: [start_cursor.x, @win.cursor.x].max
197
+ )
198
+
199
+ ev = UI::MouseSectionEvent.new(start_pos, stop_pos)
200
+ mouse_section.add(ev)
201
+ ev
202
+ end
203
+
204
+ def mouse_click(mevent)
205
+ if ev = mouse_event_transform(mevent)
206
+ ev.x += @pad_mincol
207
+ ev.y += @pad_minrow
208
+ trigger(@mouse, ev)
209
+ trigger(@mouse_section, ev)
210
+ end
211
+ end
212
+
187
213
  def refresh
188
214
  @win.noutrefresh(
189
215
  @pad_minrow, @pad_mincol,
data/lib/ektoplayer/ui.rb CHANGED
@@ -30,7 +30,7 @@ module UI
30
30
  each {|_|Curses.send(_)}
31
31
  Curses.mousemask(Curses::ALL_MOUSE_EVENTS)
32
32
  Curses.stdscr.keypad(true)
33
- [UI::Colors, UI::Input].each(&:start)
33
+ UI::Colors.start
34
34
 
35
35
  self.enable_resize_detection
36
36
  end
@@ -66,6 +66,8 @@ module UI
66
66
  Curses.clear
67
67
  Curses.addstr('terminal too small!')
68
68
  Curses.refresh
69
+ rescue
70
+ nil
69
71
  end
70
72
 
71
73
  def self.sub(cls, **opts)
@@ -92,6 +94,8 @@ module UI
92
94
  rescue UI::WidgetSizeError
93
95
  Curses.clear
94
96
  Curses.addstr('terminal too small!')
97
+ rescue
98
+ Application.log(self, $!)
95
99
  end
96
100
 
97
101
  Curses.doupdate
@@ -115,26 +119,19 @@ module UI
115
119
  KEYMAP_WORKAROUND.default_proc = proc { |h,k| k }
116
120
  KEYMAP_WORKAROUND.freeze
117
121
 
118
- def self.start
119
- @@mode = :curses
120
- Readline.input, @@readline_in_write = IO.pipe
121
- Readline.output = File.open(File::NULL, ?w)
122
- end
123
-
124
122
  #def self.getch(timeout=-1)
125
123
  # KEYMAP_WORKAROUND[@@widget.getch(timeout)]
126
124
  #end
127
125
 
128
126
  def self.start_loop
129
- @@readline_mutex ||= Mutex.new
130
- @@readline_cond ||= ConditionVariable.new
127
+ @@readline_obj ||= ReadlineWindow.new
131
128
 
132
129
  loop do
133
- if @@mode == :curses
130
+ unless @@readline_obj.active?
134
131
  Curses.curs_set(0)
135
132
  Curses.nonl
136
133
 
137
- while @@mode == :curses
134
+ begin
138
135
  UI::Canvas.widget.win.keypad=(true)
139
136
  c = KEYMAP_WORKAROUND[UI::Canvas.widget.win.getch1]
140
137
 
@@ -145,65 +142,79 @@ module UI
145
142
  elsif c # (not nil)
146
143
  UI::Canvas.widget.key_press(c.is_a?(Integer) ? c : c.to_sym)
147
144
  end
148
- end
145
+ end while !@@readline_obj.active?
149
146
  else
150
147
  Curses.curs_set(1)
151
148
  Curses.nl
152
149
 
153
- while @@mode == :readline
150
+ begin
154
151
  win = UI::Canvas.widget.win
155
152
  win.keypad=(false)
156
- c = win.getch1
153
+ next unless (c = win.getch1)
157
154
 
158
155
  if c == 10 or c == 4
159
- @@readline_thread.kill rescue nil
160
- @@mode = :curses
156
+ @@readline_obj.feed(?\n)
161
157
  else
162
- @@readline_in_write.write(c.chr)
158
+ @@readline_obj.feed(c.chr)
163
159
 
164
160
  if c == 27 # pass 3-character escape sequence
165
161
  if c = win.getch1(1)
166
- @@readline_in_write.write(c.chr)
162
+ @@readline_obj.feed(c.chr)
167
163
  if c = win.getch1(1)
168
- @@readline_in_write.write(c.chr)
164
+ @@readline_obj.feed(c.chr)
169
165
  end
170
166
  end
171
167
  end
172
168
  end
173
-
174
- @@readline_cond.signal
175
- end
169
+ end while @@readline_obj.active?
176
170
  end
177
171
  end
178
172
  end
179
173
 
180
- def self.readline(pos, size, prompt: '', add_hist: false)
181
- @@mode = :readline
174
+ def self.readline(*args, **opts, &block)
175
+ (@@readline_obj ||= ReadlineWindow.new).readline(*args, **opts, &block)
176
+ end
177
+ end
182
178
 
183
- Readline.set_screen_size(size.height, size.width)
184
- @@readline_thread ||= Thread.new do
185
- begin
186
- window = Curses::Window.new(size.height, size.width, pos.y, pos.x)
187
- rl_thread = Thread.new { Readline.delete_text; Readline.readline }
188
-
189
- while rl_thread.alive?
190
- buffer = "#{prompt}#{Readline.line_buffer}"
191
- window.erase
192
- window << buffer[(buffer.size - size.width).clamp(0, buffer.size)..-1]
193
- window.cursor=(Point.new(x: Readline.point + prompt.size, y: 0))
194
- window.refresh
195
- @@readline_mutex.synchronize { @@readline_cond.wait(@@readline_mutex, 0.3) }
196
- end
197
- ensure
198
- rl_thread.kill
199
- window.clear
200
- yield Readline.line_buffer
201
- @@mode = :curses
202
- @@readline_thread = nil
203
- Canvas.update_screen(true)
179
+ class ReadlineWindow
180
+ def initialize
181
+ @mutex, @cond = Mutex.new, ConditionVariable.new
182
+ Readline.input, @readline_in_write = IO.pipe
183
+ Readline.output = File.open(File::NULL, ?w)
184
+ @thread = nil
185
+ end
186
+
187
+ def active?; @thread; end
188
+
189
+ def readline(pos, size, prompt: '', add_hist: false, &block)
190
+ @thread ||= Thread.new do
191
+ window = Curses::Window.new(size.height, size.width, pos.y, pos.x)
192
+
193
+ Readline.set_screen_size(size.height, size.width)
194
+ Readline.delete_text
195
+ rlt = Thread.new { Readline.readline(prompt, add_hist) }
196
+
197
+ while rlt.alive?
198
+ window.erase
199
+ buffer = "#{prompt}#{Readline.line_buffer}"
200
+ window << buffer[(buffer.size - size.width).clamp(0, buffer.size)..-1]
201
+ window.cursor=(Point.new(x: Readline.point + prompt.size, y: 0))
202
+ window.refresh
203
+ @mutex.synchronize { @cond.wait(@mutex, 0.2) }
204
204
  end
205
+
206
+ window.clear
207
+ block.(Readline.line_buffer)
208
+ UI::Canvas.update_screen(true)
209
+ @thread = nil
205
210
  end
206
211
  end
212
+
213
+ def feed(c)
214
+ @readline_in_write.write(c)
215
+ @cond.signal
216
+ @thread = nil if c == ?\n
217
+ end
207
218
  end
208
219
 
209
220
  class Output
@@ -22,6 +22,7 @@ module Ektoplayer
22
22
  def update(start_url: FREE_MUSIC_URL, pages: 0, parallel: 10)
23
23
  queue = parallel > 0 ? SizedQueue.new(parallel) : Queue.new
24
24
  insert_browserpage(bp = BrowsePage.new(start_url))
25
+ results = Queue.new
25
26
 
26
27
  if pages > 0
27
28
  bp.page_urls[(bp.current_page_index + 1)..(bp.current_page_index + pages + 1)]
@@ -30,13 +31,26 @@ module Ektoplayer
30
31
  end.
31
32
  each do |url|
32
33
  queue << Thread.new do
33
- insert_browserpage(BrowsePage.new(url))
34
+ results << BrowsePage.new(url)
34
35
  queue.pop # unregister our thread
36
+ end.priority=(-10)
37
+
38
+ if results.size > 40
39
+ @db.transaction
40
+ 40.times { insert_browserpage(results.pop(true)) }
41
+ @db.commit
35
42
  end
36
43
  end
37
44
 
38
45
  sleep 1 while not queue.empty?
39
- rescue Application.log(self, $!)
46
+
47
+ @db.transaction
48
+ while (result = queue.pop(true) rescue nil)
49
+ insert_browserpage(result)
50
+ end
51
+ @db.commit
52
+ rescue
53
+ Application.log(self, $!)
40
54
  end
41
55
 
42
56
  private def insert_browserpage(browserpage)
@@ -45,7 +59,8 @@ module Ektoplayer
45
59
  end
46
60
 
47
61
  browserpage.albums.each { |album| insert_album album }
48
- rescue Application.log(self, $!)
62
+ rescue
63
+ Application.log(self, $!)
49
64
  end
50
65
 
51
66
  private def insert_album(album)
@@ -72,6 +87,8 @@ module Ektoplayer
72
87
  track_r[:album_url] = album[:url]
73
88
  @db.replace_into(:tracks, track_r)
74
89
  end
90
+ rescue
91
+ Application.log(self, $!)
75
92
  end
76
93
  end
77
94
  end
@@ -1,5 +1,4 @@
1
1
  require_relative '../ui/widgets'
2
- require_relative '../bindings'
3
2
  require_relative '../theme'
4
3
  require_relative '../common'
5
4
 
@@ -16,12 +15,13 @@ module Ektoplayer
16
15
 
17
16
  module Views
18
17
  class Info < UI::Pad
19
- def attach(playlist, trackloader, database)
20
- @playlist, @trackloader, @database = playlist, trackloader, database
18
+ def attach(player, playlist, trackloader, database)
19
+ @player, @playlist, @trackloader, @database =
20
+ player, playlist, trackloader, database
21
21
 
22
22
  Thread.new do
23
23
  loop { sleep 1; with_lock { want_redraw } }
24
- end
24
+ end.priority=(-10)
25
25
  end
26
26
 
27
27
  def draw_heading(heading)
@@ -77,33 +77,9 @@ module Ektoplayer
77
77
  end
78
78
  end
79
79
 
80
- def with_mouse_section_event
81
- start_cursor = @win.cursor; yield
82
-
83
- start_pos = UI::Point.new(
84
- y: [start_cursor.y, @win.cursor.y].min,
85
- x: [start_cursor.x, @win.cursor.x].min,
86
- )
87
- stop_pos = UI::Point.new(
88
- y: [start_cursor.y, @win.cursor.y].max,
89
- x: [start_cursor.x, @win.cursor.x].max
90
- )
91
-
92
- ev = UI::MouseSectionEvent.new(start_pos, stop_pos)
93
- mouse_section.add(ev)
94
- ev
95
- end
96
-
97
- def mouse_click(mevent)
98
- if ev = mouse_event_transform(mevent)
99
- ev.x += @pad_mincol
100
- ev.y += @pad_minrow
101
- trigger(@mouse, ev)
102
- trigger(@mouse_section, ev)
103
- end
104
- end
105
-
106
80
  def draw
81
+ return unless @player
82
+
107
83
  self.pad_size=(UI::Size.new(
108
84
  height: 200,
109
85
  width: [@size.width, MIN_WIDTH].max
@@ -121,7 +97,7 @@ module Ektoplayer
121
97
  draw_tag('Artist', @track['artist'])
122
98
  draw_tag('Album', @track['album'])
123
99
  draw_tag('BPM', @track['bpm'])
124
- draw_tag('Length', Common::to_time(@length))
100
+ draw_tag('Length', Common::to_time(@player.length))
125
101
  @win.next_line
126
102
 
127
103
  draw_heading('Current album')
@@ -137,7 +113,7 @@ module Ektoplayer
137
113
  draw_tag('Posted by'); draw_url(url, @track['posted_by'])
138
114
  end
139
115
 
140
- draw_tag('Styles', @track['styles'].sub(?,, ', '))
116
+ draw_tag('Styles', @track['styles'].gsub(?,, ', '))
141
117
  draw_tag('Downloads', @track['download_count'])
142
118
  draw_tag('Rating', "%0.2d%% (%d Votes)" % [@track['rating'], @track['votes']])
143
119
  draw_tag('Cover'); draw_url(@track['cover_url'], 'Cover')
@@ -1,61 +1,52 @@
1
- %w( ../ui/widgets/tabbedcontainer playinginfo progressbar
2
- volumemeter splash playlist browser info help ).
1
+ %w( ../ui/widgets/container playinginfo progressbar
2
+ volumemeter splash playlist browser info help tabbar ).
3
3
  each {|_|require_relative(_)}
4
4
 
5
5
  module Ektoplayer
6
6
  module Views
7
7
  class MainWindow < UI::VerticalContainer
8
- attr_reader :progressbar, :volumemeter, :playinginfo
9
- attr_reader :tabs, :splash, :playlist, :browser, :info, :help
8
+ attr_reader :progressbar, :volumemeter, :playinginfo, :tabbar
9
+ attr_reader :windows, :splash, :playlist, :browser, :info, :help
10
10
 
11
11
  def initialize(**opts)
12
12
  super(**opts)
13
13
 
14
- s1 = UI::Size.new(height: 1, width: 1) # TODO.....!!
15
-
16
14
  @playinginfo = sub(PlayingInfo)
17
15
  @progressbar = sub(ProgressBar)
18
16
  @volumemeter = sub(VolumeMeter)
19
- @tabs = sub(UI::TabbedContainer)
20
- @help = @tabs.sub(Help, size: s1, visible: false)
21
- @info = @tabs.sub(Info, size: s1, visible: false)
22
- @splash = @tabs.sub(Splash, size: s1, visible: false)
23
- @browser = @tabs.sub(Browser, size: s1, visible: false)
24
- @playlist = @tabs.sub(Playlist, size: s1, visible: false)
25
-
26
- @tabs.attributes=(
27
- %w(tab_selected tabs).map do |attr|
28
- [attr.to_sym, Theme[attr.to_sym]]
29
- end.to_h
30
- )
17
+ @tabbar = sub(TabBar)
18
+ @windows = sub(UI::SwitchContainer)
19
+ @help = @windows.sub(Help, visible: false)
20
+ @info = @windows.sub(Info, visible: false)
21
+ @splash = @windows.sub(Splash, visible: false)
22
+ @browser = @windows.sub(Browser, visible: false)
23
+ @playlist = @windows.sub(Playlist, visible: false)
31
24
 
32
25
  Config[:'tabs.widgets'].each do |widget|
33
- @tabs.add(send(widget), widget)
26
+ @windows.add(send(widget))
27
+ @tabbar.add(widget)
34
28
  end
35
29
 
36
30
  Config[:'main.widgets'].each { |w| add(send(w)) }
37
- self.selected=(@tabs)
31
+
32
+ @windows.selected=(@splash)
33
+ self.selected=(@windows)
38
34
  end
39
35
 
40
36
  def layout
41
37
  height = @size.height
42
38
 
43
- if @playinginfo.visible?
44
- @playinginfo.size=(@size.update(height: 2))
45
- height -= 2
46
- end
39
+ @playinginfo.size=(@size.update(height: 2))
40
+ @volumemeter.size=(@size.update(height: 1))
41
+ @progressbar.size=(@size.update(height: 1))
42
+ @tabbar.size=(@size.update(height: 1))
47
43
 
48
- if @volumemeter.visible?
49
- @volumemeter.size=(@size.update(height: 1))
50
- height -= 1
51
- end
52
-
53
- if @progressbar.visible?
54
- @progressbar.size=(@size.update(height: 1))
55
- height -= 1
56
- end
44
+ height -= 2 if @playinginfo.visible?
45
+ height -= 1 if @volumemeter.visible?
46
+ height -= 1 if @progressbar.visible?
47
+ height -= 1 if @tabbar.visible?
57
48
 
58
- @tabs.size=(@size.update(height: height))
49
+ @windows.size=(@size.update(height: height))
59
50
 
60
51
  super
61
52
  end
@@ -0,0 +1,53 @@
1
+ require_relative '../ui/widgets'
2
+ require_relative '../theme'
3
+
4
+ module Ektoplayer
5
+ module Views
6
+ class TabBar < UI::Pad
7
+ def initialize(**opts)
8
+ super(**opts)
9
+ events.register(:tab_clicked)
10
+ @selected, @tabs = 0, []
11
+ end
12
+
13
+ def add(title)
14
+ with_lock do
15
+ @tabs << title
16
+ want_redraw
17
+ end
18
+ end
19
+
20
+ def selected=(index)
21
+ index = index.clamp(0, @tabs.size - 1)
22
+ return if index == @selected
23
+
24
+ with_lock do
25
+ @selected = index
26
+ want_redraw
27
+ end
28
+ end
29
+
30
+ def draw
31
+ self.pad_size=(@size.update(height: 1))
32
+ mouse_section.clear
33
+ @win.erase
34
+ @win.setpos(0,0)
35
+
36
+ @tabs.each_with_index do |title, i|
37
+ mevent = with_mouse_section_event do
38
+ if i == @selected
39
+ @win.with_attr(Theme[:'tabbar.selected']) { @win << title.to_s }
40
+ else
41
+ @win.with_attr(Theme[:'tabbar.unselected']) { @win << title.to_s }
42
+ end
43
+
44
+ @win.addch(' ')
45
+ end
46
+ mevent.on(Curses::BUTTON1_CLICKED) do
47
+ trigger(@events, :tab_clicked, i)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ektoplayer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Abendroth
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-19 00:00:00.000000000 Z
11
+ date: 2017-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: audite
@@ -80,7 +80,8 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '1.2'
83
- description: Console audio player for ektoplazm.com
83
+ description: Ektoplayer is a commandline client for http://ektoplazm.com, a website
84
+ providing free electronic music such as techno, goa and psy-trance
84
85
  email: braph93@gmx.de
85
86
  executables:
86
87
  - ektoplayer
@@ -126,7 +127,6 @@ files:
126
127
  - lib/ektoplayer/ui/widgets/container.rb
127
128
  - lib/ektoplayer/ui/widgets/labelwidget.rb
128
129
  - lib/ektoplayer/ui/widgets/listwidget.rb
129
- - lib/ektoplayer/ui/widgets/tabbedcontainer.rb
130
130
  - lib/ektoplayer/updater.rb
131
131
  - lib/ektoplayer/views/browser.rb
132
132
  - lib/ektoplayer/views/help.rb
@@ -136,9 +136,10 @@ files:
136
136
  - lib/ektoplayer/views/playlist.rb
137
137
  - lib/ektoplayer/views/progressbar.rb
138
138
  - lib/ektoplayer/views/splash.rb
139
+ - lib/ektoplayer/views/tabbar.rb
139
140
  - lib/ektoplayer/views/trackrenderer.rb
140
141
  - lib/ektoplayer/views/volumemeter.rb
141
- homepage: http://www.github.com/braph/ektoplayer
142
+ homepage: http://github.com/braph/ektoplayer
142
143
  licenses:
143
144
  - GPL-3.0
144
145
  metadata: {}
@@ -161,5 +162,5 @@ rubyforge_project:
161
162
  rubygems_version: 2.6.8
162
163
  signing_key:
163
164
  specification_version: 4
164
- summary: play music from ektoplazm.com
165
+ summary: play or download music from ektoplazm.com
165
166
  test_files: []
@@ -1,110 +0,0 @@
1
- require_relative 'container'
2
- require_relative 'labelwidget'
3
-
4
- module UI
5
- class TabbedContainer < GenericContainer
6
- attr_reader :show_tabbar, :attributes
7
-
8
- def initialize(**opts)
9
- super(**opts)
10
- @show_tabbar = true
11
- @tabbar = sub(HorizontalContainer)
12
- @attributes = Hash.new { 0 }
13
- end
14
-
15
- def show_tabbar=(new)
16
- return if @show_tabbar == new
17
- with_lock { @show_tabbar = new; want_refresh }
18
- end
19
-
20
- def layout
21
- if @show_tabbar
22
- @tabbar.with_lock do
23
- @tabbar.visible!
24
- @tabbar.pos=(@pos)
25
- @tabbar.size=(@size.update(height: 1))
26
- end
27
-
28
- if @selected
29
- @selected.with_lock do
30
- @selected.size=(@size.calc(height: -1))
31
- @selected.pos=(@pos.calc(y: 1))
32
- end
33
- end
34
- else
35
- if @selected
36
- @selected.with_lock do
37
- @selected.size=(@size)
38
- @selected.pos=(@pos)
39
- end
40
- end
41
- end
42
-
43
- super
44
- end
45
-
46
- def attributes=(new)
47
- return if @attributes == new
48
- with_lock { @attributes.update(new); update_tabbar }
49
- end
50
-
51
- def visible_widgets
52
- return [@tabbar, @selected] if @show_tabbar and @selected
53
- return [@selected] if @selected
54
- return [@tabbar] if @show_tabbar
55
- return []
56
- end
57
-
58
- def add(widget, title)
59
- with_lock do
60
- super(widget)
61
- tab = @tabbar.sub(LabelWidget, text: title, pad: {left: 1})
62
- tab.fit
63
- tab.mouse.on_all { self.selected=(widget) }
64
- @tabbar.add(tab)
65
- update_tabbar
66
- end
67
- end
68
-
69
- def remove(widget)
70
- with_lock do
71
- index = @widgets.index(widget) or fail KeyError
72
- @tabbar.remove(@tabbar.widgets[index])
73
- super(widget)
74
- update_tabbar
75
- end
76
- end
77
-
78
- def selected=(widget)
79
- with_lock do
80
- (@selected.invisible!) if @selected
81
- super(widget)
82
- (@selected.visible!) if @selected
83
- update_tabbar
84
- want_layout
85
- end
86
- end
87
-
88
- def selected_index=(index)
89
- with_lock do
90
- (@selected.invisible!) if @selected
91
- super(index)
92
- update_tabbar
93
- (@selected.visible!) if @selected
94
- want_layout
95
- end
96
- end
97
-
98
- private def update_tabbar
99
- with_lock do
100
- @tabbar.widgets.each_with_index do |tab, i|
101
- if @widgets[i].equal?(@selected)
102
- tab.attributes=(@attributes[:'tab_selected'])
103
- else
104
- tab.attributes=(@attributes[:'tabs'])
105
- end
106
- end
107
- end
108
- end
109
- end
110
- end