ektoplayer 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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