ektoplayer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +49 -0
- data/bin/ektoplayer +7 -0
- data/lib/ektoplayer.rb +10 -0
- data/lib/ektoplayer/application.rb +148 -0
- data/lib/ektoplayer/bindings.rb +230 -0
- data/lib/ektoplayer/browsepage.rb +138 -0
- data/lib/ektoplayer/client.rb +18 -0
- data/lib/ektoplayer/common.rb +91 -0
- data/lib/ektoplayer/config.rb +247 -0
- data/lib/ektoplayer/controllers/browser.rb +47 -0
- data/lib/ektoplayer/controllers/controller.rb +9 -0
- data/lib/ektoplayer/controllers/help.rb +21 -0
- data/lib/ektoplayer/controllers/info.rb +22 -0
- data/lib/ektoplayer/controllers/mainwindow.rb +40 -0
- data/lib/ektoplayer/controllers/playlist.rb +60 -0
- data/lib/ektoplayer/database.rb +199 -0
- data/lib/ektoplayer/events.rb +56 -0
- data/lib/ektoplayer/models/browser.rb +127 -0
- data/lib/ektoplayer/models/database.rb +49 -0
- data/lib/ektoplayer/models/model.rb +15 -0
- data/lib/ektoplayer/models/player.rb +28 -0
- data/lib/ektoplayer/models/playlist.rb +72 -0
- data/lib/ektoplayer/models/search.rb +42 -0
- data/lib/ektoplayer/models/trackloader.rb +17 -0
- data/lib/ektoplayer/mp3player.rb +151 -0
- data/lib/ektoplayer/operations/browser.rb +19 -0
- data/lib/ektoplayer/operations/operations.rb +26 -0
- data/lib/ektoplayer/operations/player.rb +11 -0
- data/lib/ektoplayer/operations/playlist.rb +67 -0
- data/lib/ektoplayer/theme.rb +102 -0
- data/lib/ektoplayer/trackloader.rb +146 -0
- data/lib/ektoplayer/ui.rb +404 -0
- data/lib/ektoplayer/ui/colors.rb +105 -0
- data/lib/ektoplayer/ui/widgets.rb +195 -0
- data/lib/ektoplayer/ui/widgets/container.rb +125 -0
- data/lib/ektoplayer/ui/widgets/labelwidget.rb +43 -0
- data/lib/ektoplayer/ui/widgets/listwidget.rb +332 -0
- data/lib/ektoplayer/ui/widgets/tabbedcontainer.rb +110 -0
- data/lib/ektoplayer/updater.rb +77 -0
- data/lib/ektoplayer/views/browser.rb +25 -0
- data/lib/ektoplayer/views/help.rb +46 -0
- data/lib/ektoplayer/views/info.rb +208 -0
- data/lib/ektoplayer/views/mainwindow.rb +64 -0
- data/lib/ektoplayer/views/playinginfo.rb +135 -0
- data/lib/ektoplayer/views/playlist.rb +39 -0
- data/lib/ektoplayer/views/progressbar.rb +51 -0
- data/lib/ektoplayer/views/splash.rb +99 -0
- data/lib/ektoplayer/views/trackrenderer.rb +137 -0
- data/lib/ektoplayer/views/volumemeter.rb +74 -0
- 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
|