ektoplayer 0.1.0

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.
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
@@ -0,0 +1,332 @@
1
+ require_relative '../widgets'
2
+
3
+ module UI
4
+ class ListItemRenderer
5
+ def initialize(width: nil)
6
+ @width = width
7
+ end
8
+
9
+ def width=(new)
10
+ @width != new and (@width = new; layout)
11
+ end
12
+
13
+ def layout; end
14
+
15
+ def render(scr, item, selected: false, marked: false)
16
+ scr << (selected ? ?> : ' ')
17
+ scr << item_to.s
18
+ end
19
+ end
20
+
21
+ class ListSearch
22
+ attr_accessor :direction, :source
23
+
24
+ def initialize(search: '', source: [], direction: :down)
25
+ @source, @result, @current = source, [], 0
26
+ @direction = direction
27
+ self.search=(search)
28
+ end
29
+
30
+ def comp(item, search)
31
+ if item.is_a?String
32
+ return item.downcase =~ Regexp.new(search.downcase)
33
+ elsif item.is_a?Hash
34
+ %w(title artist album).each do |key|
35
+ return true if self.comp(item[key], search)
36
+ end
37
+ end
38
+
39
+ false
40
+ end
41
+
42
+ def search=(search)
43
+ fail unless search
44
+ @search = search
45
+ @current = 0
46
+ @result = @source.size.times.select {|i| self.comp(@source[i], search) }
47
+ end
48
+
49
+ def current; @result[@current] or 0 end
50
+ def next; @direction == :up ? search_up : search_down end
51
+ def prev; @direction == :up ? search_down : search_up end
52
+
53
+ def search_up
54
+ @current -= 1
55
+ @current = @result.size - 1 if @current < 0
56
+ self
57
+ end
58
+
59
+ def search_down
60
+ @current = 0 if (@current += 1) >= @result.size
61
+ self
62
+ end
63
+ end
64
+
65
+ class ListWidget < Window
66
+ attr_reader :list, :selected, :cursor
67
+ attr_accessor :item_renderer
68
+
69
+ def initialize(list: [], item_renderer: nil, **opts)
70
+ super(**opts)
71
+ self.list=(list)
72
+ @item_renderer = (item_renderer or ListItemRenderer.new)
73
+ @cursor = @selected = 0
74
+ @search = ListSearch.new
75
+ end
76
+
77
+ def search_next; self.selected=(@search.next.current) end
78
+ def search_prev; self.selected=(@search.prev.current) end
79
+ def search_up; self.search_start(:up) end
80
+ def search_down; self.search_start(:down) end
81
+ def search_start(direction=:down)
82
+ UI::Input.readline(@pos, @size.update(height: 1), prompt: '> ', add_hist: true) do |result|
83
+ if result
84
+ @search.source=(@list)
85
+ @search.direction=(direction)
86
+ @search.search=(result)
87
+ search_next
88
+ end
89
+ end
90
+ end
91
+
92
+ def render(index, **opts)
93
+ return unless @item_renderer
94
+ return unless @list[index] # TODO...?
95
+ @item_renderer.render(@win, @list[index], index, **opts)
96
+ end
97
+
98
+ def layout
99
+ @item_renderer.width = @size.width if @item_renderer
100
+ super
101
+ end
102
+
103
+ def top; self.selected=(0) end
104
+ def bottom; self.selected=(index_last) end
105
+ def page_up; self.scroll_up(size.height) end
106
+ def page_down; self.scroll_down(size.height) end
107
+ def up; self.selected=(selected - 1) end
108
+ def down; self.selected=(selected + 1) end
109
+ def center; self.force_cursorpos(@size.height / 2) end
110
+
111
+ def list=(list)
112
+ with_lock do
113
+ @list = list
114
+ @cursor = @selected = 0
115
+ self.selected=(0)
116
+ want_redraw
117
+ end
118
+ end
119
+
120
+ def selected=(new_index)
121
+ fail ArgumentError unless new_index
122
+ new_index = new_index.clamp(0, index_last)
123
+ return if @selected == new_index or @list.empty?
124
+
125
+ self.lock
126
+
127
+ new_cursor = @cursor + new_index - @selected
128
+ if new_cursor.between?(0, @size.height - 1)
129
+ # new selected item resides in current screen,
130
+ # just want_redraw the old line and the newly selected one
131
+ write_at(@cursor); render(@selected)
132
+ write_at(new_cursor); render(new_index, selected: true)
133
+ @selected, @cursor = new_index, new_cursor
134
+ _check
135
+ want_refresh
136
+ elsif (new_cursor.between?(-(@size.height - 1), (2 * @size.height - 1)))
137
+ # new selected item is max a half screen size away
138
+ if new_index < @selected
139
+ if lines_after_cursor > (@selected - new_index)
140
+ write_at(@cursor); render(@selected)
141
+ end
142
+
143
+ (index_top - 1).downto(new_index + 1).each do |index|
144
+ @win.insert_top; render(index)
145
+ end
146
+
147
+ @win.insert_top; render(new_index, selected: true)
148
+ @selected, @cursor = new_index, 0
149
+ _check
150
+ else
151
+ if lines_before_cursor > (new_index - @selected)
152
+ write_at(@cursor); render(@selected)
153
+ end
154
+
155
+ (index_bottom + 1).upto(new_index - 1).each do |index|
156
+ @win.append_bottom; render(index)
157
+ end
158
+
159
+ @win.append_bottom; render(new_index, selected: true)
160
+ @selected, @cursor = new_index, cursor_max
161
+ _check
162
+ end
163
+
164
+ want_refresh
165
+ else
166
+ @selected = new_index
167
+ @cursor = new_index.clamp(0, cursor_max)
168
+ _check
169
+ want_redraw
170
+ end
171
+
172
+ self.unlock
173
+ end
174
+
175
+ # select an item by its current cursor pos
176
+ def select_from_cursorpos(new_cursor)
177
+ fail unless new_cursor.between?(0, cursor_max)
178
+ # FIXME: clamp with @list.size ????
179
+ return if (new_cursor == @cursor) or @list.empty?
180
+
181
+ with_lock do
182
+ new_index = (@selected - (@cursor - new_cursor)).clamp(0, index_last)
183
+ write_at(@cursor); render(@selected)
184
+ write_at(new_cursor); render(new_index, selected: true)
185
+ @selected, @cursor = new_index, new_cursor
186
+ _check
187
+ want_refresh
188
+ end
189
+ end
190
+
191
+ def force_cursorpos(new_cursor)
192
+ self.lock
193
+ if @selected <= cursor_max
194
+ @cursor = @selected
195
+ elsif (diff = (index_last - @selected)) < cursor_max
196
+ @cursor = @size.height - diff - 1 #cursor_max.clamp(0, index_last - @selected)
197
+ else
198
+ @cursor = new_cursor.clamp(0, cursor_max)
199
+ end
200
+ want_redraw
201
+ self.unlock
202
+ end
203
+
204
+ def scroll_up(n=1)
205
+ fail ArgumentError unless n
206
+ n = n.clamp(0, items_before_cursor)
207
+ return if n == 0 or @list.empty?
208
+ self.lock
209
+
210
+ if index_top == 0
211
+ # list is already on top
212
+ select_from_cursorpos((@cursor - n).clamp(0, cursor_max))
213
+ elsif n < @size.height
214
+ if lines_after_cursor > n
215
+ write_at(@cursor); render(@selected)
216
+ end
217
+
218
+ (index_top - 1).downto(index_top - n).each do |index|
219
+ @win.insert_top; render(index)
220
+ end
221
+
222
+ @selected -= n
223
+ write_at(@cursor); render(@selected, selected: true)
224
+
225
+ _check
226
+ want_refresh
227
+ else
228
+ @selected -= n
229
+ force_cursorpos(@cursor)
230
+ _check
231
+ want_redraw
232
+ end
233
+
234
+ self.unlock
235
+ end
236
+
237
+ def scroll_down(n=1)
238
+ fail ArgumentError unless n
239
+ n = n.clamp(0, items_after_cursor)
240
+ return if n == 0 or @list.empty?
241
+ self.lock
242
+
243
+ if index_bottom == index_last
244
+ select_from_cursorpos((@cursor + n).clamp(0, cursor_max))
245
+ _check
246
+ elsif n < @size.height
247
+ if lines_before_cursor > n
248
+ write_at(@cursor); render(@selected)
249
+ end
250
+
251
+ (index_bottom + 1).upto(index_bottom + n).each do |index|
252
+ @win.append_bottom; render(index)
253
+ end
254
+
255
+ @selected += n
256
+ write_at(@cursor); render(@selected, selected: true)
257
+
258
+ _check
259
+ want_refresh
260
+ else
261
+ @selected += n
262
+ force_cursorpos(@cursor)
263
+ _check
264
+ want_redraw
265
+ end
266
+
267
+ self.unlock
268
+ _check
269
+ end
270
+
271
+ def draw
272
+ @win.erase
273
+ return if @list.empty?
274
+ @selected = @selected.clamp(0, index_last)
275
+ _check
276
+
277
+ @cursor.times do |i|
278
+ unless row = @list[@selected - (@cursor - i)]
279
+ @cursor = i
280
+ break
281
+ end
282
+
283
+ write_at(i); render(@selected - (@cursor - i))
284
+ end
285
+
286
+ _check
287
+ write_at(@cursor); render(@selected, selected: true)
288
+
289
+ (@cursor + 1).upto(@size.height - 1).each_with_index do |c, i|
290
+ break unless row = @list[@selected + i + 1]
291
+ write_at(c); render(@selected + i + 1)
292
+ end
293
+
294
+ _check
295
+ end
296
+
297
+ def on_mouse_click(mevent, mevent_transformed)
298
+ if new_mouse = mouse_event_transform(mevent)
299
+ select_from_cursorpos(new_mouse.y)
300
+ end
301
+ super(mevent)
302
+ end
303
+
304
+ protected
305
+
306
+ def write_at(pos) @win.line_start(pos).clrtoeol end
307
+
308
+ def index_first; 0 end
309
+ def index_last; [@list.size, 1].max - 1 end
310
+ def index_top; @selected - @cursor end
311
+ def index_bottom
312
+ [@selected + @size.height - @cursor, @list.size].min - 1
313
+ end
314
+
315
+ def lines_before_cursor; @cursor end
316
+ def lines_after_cursor; @size.height - cursor - 1 end
317
+ def items_before_cursor; @selected; end
318
+ def items_after_cursor; @list.size - @selected - 1 end
319
+ def cursor_min; 0 end
320
+ def cursor_max; [@size.height, @list.size].min - 1 end
321
+
322
+ private def _check # debug method
323
+ return
324
+ fail "@selected = nil" unless @selected
325
+ fail "@selected = #{@selected}" unless @selected >= 0
326
+ fail "@selected > @list.size" if @selected >= @list.size
327
+ fail "@cursor = nil" unless @cursor
328
+ fail "@cursor = #{@cursor}" unless @cursor >= 0
329
+ fail "@cursor > max" if @cursor > @win.maxy
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,110 @@
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
@@ -0,0 +1,77 @@
1
+ require 'set'
2
+ require 'thread'
3
+ require 'open-uri'
4
+
5
+ require_relative 'browsepage'
6
+
7
+ module Ektoplayer
8
+ MAIN_URL = 'http://www.ektoplazm.com'.freeze
9
+ FREE_MUSIC_URL = "#{MAIN_URL}/section/free-music".freeze
10
+
11
+ class DatabaseUpdater
12
+ ALBUM_STR_TAGS = Set.new(%w(url title date category cover_url
13
+ description download_count rating votes
14
+ released_by released_by_url posted_by posted_by_url).map(&:to_sym)).freeze
15
+
16
+ TRACK_STR_TAGS = Set.new(%w(url number title remix artist bpm).map(&:to_sym)).freeze
17
+
18
+ def initialize(db)
19
+ @db = db
20
+ end
21
+
22
+ def update(start_url: FREE_MUSIC_URL, pages: 0, parallel: 10)
23
+ queue = parallel > 0 ? SizedQueue.new(parallel) : Queue.new
24
+ insert_browserpage(bp = BrowsePage.new(start_url))
25
+
26
+ if pages > 0
27
+ bp.page_urls[(bp.current_page_index + 1)..(bp.current_page_index + pages + 1)]
28
+ else
29
+ bp.page_urls[(bp.current_page_index + 1)..-1]
30
+ end.
31
+ each do |url|
32
+ queue << Thread.new do
33
+ insert_browserpage(BrowsePage.new(url))
34
+ queue.pop # unregister our thread
35
+ end
36
+ end
37
+
38
+ sleep 1 while not queue.empty?
39
+ rescue Application.log(self, $!)
40
+ end
41
+
42
+ private def insert_browserpage(browserpage)
43
+ browserpage.styles.each do |style, url|
44
+ @db.replace_into(:styles, { style: style, url: url })
45
+ end
46
+
47
+ browserpage.albums.each { |album| insert_album album }
48
+ rescue Application.log(self, $!)
49
+ end
50
+
51
+ private def insert_album(album)
52
+ album_r = ALBUM_STR_TAGS.map { |tag| [tag, album[tag]] }.to_h
53
+ @db.replace_into(:albums, album_r)
54
+
55
+ album[:styles].each do |style|
56
+ @db.replace_into(:albums_styles, {
57
+ album_url: album[:url],
58
+ style: style
59
+ })
60
+ end
61
+
62
+ album[:archive_urls].each do |type, url|
63
+ @db.replace_into(:archive_urls, {
64
+ album_url: album[:url],
65
+ archive_type: type,
66
+ archive_url: url
67
+ })
68
+ end
69
+
70
+ album[:tracks].each do |track|
71
+ track_r = TRACK_STR_TAGS.map { |tag| [tag, track[tag]] }.to_h
72
+ track_r[:album_url] = album[:url]
73
+ @db.replace_into(:tracks, track_r)
74
+ end
75
+ end
76
+ end
77
+ end