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
@@ -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