diff options
-rw-r--r-- | apps/plugins/lua_scripts/dbgettags.lua | 15 | ||||
-rw-r--r-- | apps/plugins/lua_scripts/random_playlist.lua | 464 |
2 files changed, 472 insertions, 7 deletions
diff --git a/apps/plugins/lua_scripts/dbgettags.lua b/apps/plugins/lua_scripts/dbgettags.lua index ec6e29a330..8e9f26393d 100644 --- a/apps/plugins/lua_scripts/dbgettags.lua +++ b/apps/plugins/lua_scripts/dbgettags.lua | |||
@@ -28,13 +28,14 @@ local CANCEL_BUTTON = rb.actions.PLA_CANCEL | |||
28 | local sINVALIDDATABASE = "Invalid Database" | 28 | local sINVALIDDATABASE = "Invalid Database" |
29 | local sERROROPENING = "Error opening" | 29 | local sERROROPENING = "Error opening" |
30 | 30 | ||
31 | -- tag cache header | 31 | -- tag cache header |
32 | local sTCVERSION = string.char(0x0F) | 32 | sTCVERSION = string.char(0x10) |
33 | local sTCHEADER = string.reverse("TCH" .. sTCVERSION) | 33 | sTCHEADER = string.reverse("TCH" .. sTCVERSION) |
34 | local DATASZ = 4 -- int32_t | 34 | DATASZ = 4 -- int32_t |
35 | local TCHSIZE = 3 * DATASZ -- 3 x int32_t | 35 | TCHSIZE = 3 * DATASZ -- 3 x int32_t |
36 | 36 | ||
37 | local function bytesLE_n(str) | 37 | -- Converts array of bytes to proper endian |
38 | function bytesLE_n(str) | ||
38 | str = str or "" | 39 | str = str or "" |
39 | local tbyte={str:byte(1, -1)} | 40 | local tbyte={str:byte(1, -1)} |
40 | local bpos = 1 | 41 | local bpos = 1 |
diff --git a/apps/plugins/lua_scripts/random_playlist.lua b/apps/plugins/lua_scripts/random_playlist.lua new file mode 100644 index 0000000000..6e4dbe25e2 --- /dev/null +++ b/apps/plugins/lua_scripts/random_playlist.lua | |||
@@ -0,0 +1,464 @@ | |||
1 | --[[ Lua RB Random Playlist -- random_playlist.lua V 1.0 | ||
2 | /*************************************************************************** | ||
3 | * __________ __ ___. | ||
4 | * Open \______ \ ____ ____ | | _\_ |__ _______ ___ | ||
5 | * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / | ||
6 | * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < | ||
7 | * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ | ||
8 | * \/ \/ \/ \/ \/ | ||
9 | * $Id$ | ||
10 | * | ||
11 | * Copyright (C) 2021 William Wilgus | ||
12 | * | ||
13 | * This program is free software; you can redistribute it and/or | ||
14 | * modify it under the terms of the GNU General Public License | ||
15 | * as published by the Free Software Foundation; either version 2 | ||
16 | * of the License, or (at your option) any later version. | ||
17 | * | ||
18 | * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY | ||
19 | * KIND, either express or implied. | ||
20 | * | ||
21 | ****************************************************************************/ | ||
22 | ]] | ||
23 | require ("actions") | ||
24 | require("dbgettags") | ||
25 | get_tags = nil -- unneeded | ||
26 | |||
27 | -- User defaults | ||
28 | local playlistpath = "/Playlists" | ||
29 | local max_tracks = 500; -- size of playlist to create | ||
30 | local min_repeat = 500; -- this many songs before a repeat | ||
31 | local play_on_success = true; | ||
32 | |||
33 | -- Random integer function | ||
34 | local random = math.random; -- ref random(min, max) | ||
35 | math.randomseed(rb.current_tick()); -- some kind of randomness | ||
36 | |||
37 | -- Button definitions | ||
38 | local CANCEL_BUTTON = rb.actions.PLA_CANCEL | ||
39 | local OK_BUTTON = rb.actions.PLA_SELECT | ||
40 | local ADD_BUTTON = rb.actions.PLA_UP | ||
41 | local ADD_BUTTON_RPT = rb.actions.PLA_UP_REPEAT or ADD_BUTTON | ||
42 | local SUB_BUTTON = rb.actions.PLA_DOWN | ||
43 | local SUB_BUTTON_RPT = rb.actions.PLA_DOWN_REPEAT or SUB_BUTTON | ||
44 | -- remove action and context tables to free some ram | ||
45 | rb.actions = nil | ||
46 | rb.contexts = nil | ||
47 | -- Program strings | ||
48 | local sINITDATABASE = "Initialize Database" | ||
49 | local sHEADERTEXT = "Random Playlist" | ||
50 | local sPLAYLISTERROR = "Playlist Error!" | ||
51 | local sREMOVEPLAYLIST = "Removing Dynamic Playlist" | ||
52 | local sSEARCHINGFILES = "Searching for Files.." | ||
53 | local sERROROPENFMT = "Error Opening %s" | ||
54 | local sINVALIDDBFMT = "Invalid Database %s" | ||
55 | local sPROGRESSHDRFMT = "%d \\ %d Tracks" | ||
56 | local sGOODBYE = "Goodbye" | ||
57 | |||
58 | -- Gets size of text | ||
59 | local function text_extent(msg, font) | ||
60 | font = font or rb.FONT_UI | ||
61 | return rb.font_getstringsize(msg, font) | ||
62 | end | ||
63 | |||
64 | local function _setup_random_playlist(tag_entries, play, min_repeat, trackcount) | ||
65 | -- Setup string tables | ||
66 | local tPLAYTEXT = {"Play? [ %s ] (up/dn)", "true = play tracks on success"} | ||
67 | local tREPEATTEXT = {"Repeat hist? [ %d ] (up/dn)","higher = less repeated songs"} | ||
68 | local tPLSIZETEXT = {"Find [ %d ] tracks? (up/dn)", | ||
69 | "Warning overwrites dynamic playlist", | ||
70 | "Press back to cancel"}; | ||
71 | -- how many lines can we fit on the screen? | ||
72 | local res, w, h = text_extent("I") | ||
73 | h = h + 5 -- increase spacing in the setup menu | ||
74 | local max_w = rb.LCD_WIDTH / w | ||
75 | local max_h = rb.LCD_HEIGHT - h | ||
76 | local y = 0 | ||
77 | |||
78 | -- User Setup Menu | ||
79 | local action, ask, increment | ||
80 | local t_desc = {scroll = true} -- scroll the setup items | ||
81 | |||
82 | -- Clears screen and adds title and icon, called first.. | ||
83 | function show_setup_header() | ||
84 | local desc = {icon = 2, show_icons = true, scroll = true} -- 2 == Icon_Playlist | ||
85 | rb.lcd_clear_display() | ||
86 | rb.lcd_put_line(1, 0, sHEADERTEXT, desc) | ||
87 | end | ||
88 | |||
89 | -- Display up to 3 items and waits for user action -- returns action | ||
90 | function ask_user_action(desc, ln1, ln2, ln3) | ||
91 | if ln1 then rb.lcd_put_line(1, h, ln1, desc) end | ||
92 | if ln2 then rb.lcd_put_line(1, h + h, ln2, desc) end | ||
93 | if ln3 then rb.lcd_put_line(1, h + h + h, ln3, desc) end | ||
94 | rb.lcd_hline(1,rb.LCD_WIDTH - 1, h - 5); | ||
95 | rb.lcd_update() | ||
96 | |||
97 | local act = rb.get_plugin_action(-1); -- Blocking wait for action | ||
98 | -- handle magnitude of the increment here so consumer fn doesn't need to | ||
99 | if act == ADD_BUTTON_RPT and act ~= ADD_BUTTON then | ||
100 | increment = increment + 1 | ||
101 | if increment > 1000 then increment = 1000 end | ||
102 | act = ADD_BUTTON | ||
103 | elseif act == SUB_BUTTON_RPT and act ~= SUB_BUTTON then | ||
104 | increment = increment + 1 | ||
105 | if increment > 1000 then increment = 1000 end | ||
106 | act = SUB_BUTTON | ||
107 | else | ||
108 | increment = 1; | ||
109 | end | ||
110 | |||
111 | return act | ||
112 | end | ||
113 | |||
114 | -- Play the playlist on successful completion true/false? | ||
115 | function setup_get_play() | ||
116 | action = ask_user_action(tdesc, | ||
117 | string.format(tPLAYTEXT[1], tostring(play)), | ||
118 | tPLAYTEXT[2]); | ||
119 | if action == ADD_BUTTON then | ||
120 | play = true | ||
121 | elseif action == SUB_BUTTON then | ||
122 | play = false | ||
123 | end | ||
124 | end | ||
125 | |||
126 | -- Repeat song buffer list of previously added tracks 0-?? | ||
127 | function setup_get_repeat() | ||
128 | if min_repeat >= trackcount then min_repeat = trackcount - 1 end | ||
129 | if min_repeat >= tag_entries then min_repeat = tag_entries - 1 end | ||
130 | action = ask_user_action(t_desc, | ||
131 | string.format(tREPEATTEXT[1],min_repeat), | ||
132 | tREPEATTEXT[2]); | ||
133 | if action == ADD_BUTTON then | ||
134 | min_repeat = min_repeat + increment | ||
135 | elseif action == SUB_BUTTON then -- MORE REPEATS LESS RAM USED | ||
136 | if min_repeat < increment then increment = 1 end | ||
137 | min_repeat = min_repeat - increment | ||
138 | if min_repeat < 0 then min_repeat = 0 end | ||
139 | elseif action == OK_BUTTON then | ||
140 | ask = setup_get_play; | ||
141 | setup_get_repeat = nil | ||
142 | action = 0 | ||
143 | end | ||
144 | end | ||
145 | |||
146 | -- How many tracks to find | ||
147 | function setup_get_playlist_size() | ||
148 | action = ask_user_action(t_desc, | ||
149 | string.format(tPLSIZETEXT[1], trackcount), | ||
150 | tPLSIZETEXT[2], | ||
151 | tPLSIZETEXT[3]); | ||
152 | if action == ADD_BUTTON then | ||
153 | trackcount = trackcount + increment | ||
154 | elseif action == SUB_BUTTON then | ||
155 | if trackcount < increment then increment = 1 end | ||
156 | trackcount = trackcount - increment | ||
157 | if trackcount < 1 then trackcount = 1 end | ||
158 | elseif action == OK_BUTTON then | ||
159 | ask = setup_get_repeat; | ||
160 | setup_get_playlist_size = nil | ||
161 | action = 0 | ||
162 | end | ||
163 | end | ||
164 | ask = setup_get_playlist_size; -- \!FIRSTRUN!/ | ||
165 | |||
166 | repeat -- SETUP MENU LOOP | ||
167 | show_setup_header() | ||
168 | ask() | ||
169 | rb.lcd_scroll_stop() -- I'm still wary of not doing this.. | ||
170 | collectgarbage("collect") | ||
171 | if action == CANCEL_BUTTON then rb.lcd_scroll_stop(); return nil end | ||
172 | until (action == OK_BUTTON) | ||
173 | |||
174 | return play, min_repeat, trackcount; | ||
175 | end | ||
176 | |||
177 | --[[ Given the filenameDB file [database] | ||
178 | creates a random dynamic playlist with a default savename of [playlist] | ||
179 | containing [trackcount] tracks, played on completion if [play] is true]] | ||
180 | function create_random_playlist(database, playlist, trackcount, play) | ||
181 | if not database or not playlist or not trackcount then return end | ||
182 | if not play then play = false end | ||
183 | |||
184 | local file = io.open('/' .. database or "", "r") --read | ||
185 | if not file then rb.splash(100, string.format(sERROROPENFMT, database)) return end | ||
186 | |||
187 | local fsz = file:seek("end") | ||
188 | local fbegin | ||
189 | local posln = 0 | ||
190 | local tag_len = TCHSIZE | ||
191 | |||
192 | local anchor_index | ||
193 | local ANCHOR_INTV | ||
194 | local track_index = setmetatable({},{__mode = "v"}) --[[ weak table values | ||
195 | this allows them to be garbage collected as space is needed / rebuilt as needed ]] | ||
196 | |||
197 | -- Read character function sets posln as file position | ||
198 | function readchrs(count) | ||
199 | if posln >= fsz then return nil end | ||
200 | file:seek("set", posln) | ||
201 | posln = posln + count | ||
202 | return file:read(count) | ||
203 | end | ||
204 | |||
205 | -- Check the header and get size + #entries | ||
206 | local tagcache_header = readchrs(DATASZ) or "" | ||
207 | local tagcache_sz = readchrs(DATASZ) or "" | ||
208 | local tagcache_entries = readchrs(DATASZ) or "" | ||
209 | |||
210 | if tagcache_header ~= sTCHEADER or | ||
211 | bytesLE_n(tagcache_sz) ~= (fsz - TCHSIZE) then | ||
212 | rb.splash(100, string.format(sINVALIDDBFMT, database)) | ||
213 | return | ||
214 | end | ||
215 | |||
216 | local tag_entries = bytesLE_n(tagcache_entries) | ||
217 | |||
218 | play, min_repeat, trackcount = _setup_random_playlist( | ||
219 | tag_entries, play, min_repeat, trackcount); | ||
220 | _setup_random_playlist = nil | ||
221 | collectgarbage("collect") | ||
222 | |||
223 | -- how many lines can we fit on the screen? | ||
224 | local res, w, h = text_extent("I") | ||
225 | local max_w = rb.LCD_WIDTH / w | ||
226 | local max_h = rb.LCD_HEIGHT - h | ||
227 | local y = 0 | ||
228 | rb.lcd_clear_display() | ||
229 | |||
230 | function get_tracks_random() | ||
231 | local tries, idxp | ||
232 | |||
233 | local tracks = 0 | ||
234 | local str = "" | ||
235 | local t_lru = {} | ||
236 | local lru_widx = 1 | ||
237 | local lru_max = min_repeat | ||
238 | if lru_max >= tag_entries then lru_max = tag_entries / 2 + 1 end | ||
239 | |||
240 | function do_progress_header() | ||
241 | rb.lcd_put_line(1, 0, string.format(sPROGRESSHDRFMT,tracks, trackcount)) | ||
242 | rb.lcd_update() | ||
243 | --rb.sleep(300) | ||
244 | end | ||
245 | |||
246 | function show_progress() | ||
247 | local sdisp = str:match("([^/]+)$") or "?" --just the track name | ||
248 | rb.lcd_put_line(1, y, sdisp:sub(1, max_w));-- limit string length | ||
249 | y = y + h | ||
250 | if y >= max_h then | ||
251 | do_progress_header() | ||
252 | rb.lcd_clear_display() | ||
253 | y = h | ||
254 | end | ||
255 | end | ||
256 | |||
257 | -- check for repeated tracks | ||
258 | function check_lru(val) | ||
259 | if lru_max <= 0 or val == nil then return 0 end --user wants all repeats | ||
260 | local rv | ||
261 | local i = 1 | ||
262 | repeat | ||
263 | rv = t_lru[i] | ||
264 | if rv == nil then | ||
265 | break; | ||
266 | elseif rv == val then | ||
267 | return i | ||
268 | end | ||
269 | i = i + 1 | ||
270 | until (i == lru_max) | ||
271 | return 0 | ||
272 | end | ||
273 | |||
274 | -- add a track to the repeat list (overwrites oldest if full) | ||
275 | function push_lru(val) | ||
276 | t_lru[lru_widx] = val | ||
277 | lru_widx = lru_widx + 1 | ||
278 | if lru_widx > lru_max then lru_widx = 1 end | ||
279 | end | ||
280 | |||
281 | function get_index() | ||
282 | if ANCHOR_INTV > 1 then | ||
283 | get_index = | ||
284 | function(plidx) | ||
285 | local p = track_index[plidx] | ||
286 | if p == nil then | ||
287 | parse_database_offsets(plidx) | ||
288 | end | ||
289 | return track_index[plidx][1] | ||
290 | end | ||
291 | else -- all tracks are indexed | ||
292 | get_index = | ||
293 | function(plidx) | ||
294 | return track_index[plidx] | ||
295 | end | ||
296 | end | ||
297 | end | ||
298 | |||
299 | get_index() --init get_index fn | ||
300 | -- Playlist insert loop | ||
301 | while true do | ||
302 | str = nil | ||
303 | tries = 0 | ||
304 | repeat | ||
305 | idxp = random(1, tag_entries) | ||
306 | tries = tries + 1 -- prevent endless loops | ||
307 | until check_lru(idxp) == 0 or tries > fsz -- check for recent repeats | ||
308 | |||
309 | posln = get_index(idxp) | ||
310 | |||
311 | tag_len = bytesLE_n(readchrs(DATASZ)) | ||
312 | posln = posln + DATASZ -- idx = bytesLE_n(readchrs(DATASZ)) | ||
313 | str = readchrs(tag_len) or "\0" -- Read the database string | ||
314 | str = str:match("^(%Z+)%z$") -- \0 terminated string | ||
315 | |||
316 | -- Insert track into playlist | ||
317 | if str ~= nil then | ||
318 | tracks = tracks + 1 | ||
319 | show_progress() | ||
320 | push_lru(idxp) -- add to repeat list | ||
321 | if rb.playlist("insert_track", str) < 0 then | ||
322 | rb.splash(rb.HZ, sPLAYLISTERROR) | ||
323 | break; -- ERROR, PLAYLIST FULL? | ||
324 | end | ||
325 | |||
326 | end | ||
327 | |||
328 | if tracks >= trackcount then | ||
329 | do_progress_header() | ||
330 | break | ||
331 | end | ||
332 | |||
333 | -- check for cancel non-blocking | ||
334 | if rb.get_plugin_action(0) == CANCEL_BUTTON then | ||
335 | break | ||
336 | end | ||
337 | end | ||
338 | end -- get_files | ||
339 | |||
340 | function build_anchor_index() | ||
341 | -- index every n files | ||
342 | ANCHOR_INTV = 1 -- for small db we can put all the entries in ram | ||
343 | local ent = tag_entries / 1000 -- more than 10,000 will be incrementally loaded | ||
344 | while ent >= 10 do -- need to reduce the size of the anchor index? | ||
345 | ent = ent / 10 | ||
346 | ANCHOR_INTV = ANCHOR_INTV * 10 | ||
347 | end -- should be power of 10 (10, 100, 1000..) | ||
348 | --grab an index for every ANCHOR_INTV entries | ||
349 | local aidx={} | ||
350 | local acount = 0 | ||
351 | local next_idx = 1 | ||
352 | local index = 1 | ||
353 | local tlen | ||
354 | if ANCHOR_INTV == 1 then acount = 1 end | ||
355 | while index <= tag_entries and posln < fsz do | ||
356 | if next_idx == index then | ||
357 | acount = acount + 1 | ||
358 | next_idx = acount * ANCHOR_INTV | ||
359 | aidx[index] = posln | ||
360 | else -- fill the weak table, we already did the work afterall | ||
361 | track_index[index] = {posln} -- put vals inside table to make them collectable | ||
362 | end | ||
363 | index = index + 1 | ||
364 | tlen = bytesLE_n(readchrs(DATASZ)) | ||
365 | posln = posln + tlen + DATASZ | ||
366 | end | ||
367 | return aidx | ||
368 | end | ||
369 | |||
370 | function parse_database_offsets(plidx) | ||
371 | local tlen | ||
372 | -- round to nearest anchor entry that is less than plidx | ||
373 | local aidx = (plidx / ANCHOR_INTV) * ANCHOR_INTV | ||
374 | local cidx = aidx | ||
375 | track_index[cidx] = {anchor_index[aidx] or fbegin}; | ||
376 | -- maybe we can use previous work to get closer to the desired offset | ||
377 | while track_index[cidx] ~= nil and cidx <= plidx do | ||
378 | cidx = cidx + 1 --keep seeking till we find an empty entry | ||
379 | end | ||
380 | posln = track_index[cidx - 1][1] | ||
381 | while cidx <= plidx do --[[ walk the remaining entries from the last known | ||
382 | & save the entries on the way to our desired entry ]] | ||
383 | tlen = bytesLE_n(readchrs(DATASZ)) | ||
384 | posln = posln + tlen + DATASZ | ||
385 | track_index[cidx] = {posln} -- put vals inside table to make them collectable | ||
386 | if posln >= fsz then posln = fbegin end | ||
387 | cidx = cidx + 1 | ||
388 | end | ||
389 | end | ||
390 | |||
391 | if trackcount ~= nil then | ||
392 | rb.splash(10, sSEARCHINGFILES) | ||
393 | fbegin = posln --Mark the beginning for later loops | ||
394 | tag_len = 0 | ||
395 | anchor_index = build_anchor_index() -- index track offsets | ||
396 | if ANCHOR_INTV == 1 then | ||
397 | -- all track indexes are in ram | ||
398 | track_index = anchor_index | ||
399 | anchor_index = nil | ||
400 | end | ||
401 | |||
402 | rb.splash(10, sREMOVEPLAYLIST) | ||
403 | rb.audio("stop") | ||
404 | os.remove( playlistpath .. "/" .. playlist) | ||
405 | rb.playlist("remove_all_tracks") | ||
406 | rb.playlist("create", playlistpath .. "/", playlist) | ||
407 | --[[ --profiling | ||
408 | local starttime = rb.current_tick(); | ||
409 | get_tracks_random() | ||
410 | local endtime = rb.current_tick(); | ||
411 | rb.splash(1000, (endtime - starttime) .. " ticks"); | ||
412 | end | ||
413 | if (false) then | ||
414 | --]] | ||
415 | get_tracks_random() | ||
416 | end | ||
417 | |||
418 | file:close() | ||
419 | collectgarbage("collect") | ||
420 | if trackcount and rb.playlist("amount") >= trackcount and play == true then | ||
421 | rb.playlist("start", 0, 0, 0) | ||
422 | end | ||
423 | |||
424 | end -- create_playlist | ||
425 | |||
426 | local function main() | ||
427 | if not rb.file_exists(rb.ROCKBOX_DIR .. "/database_4.tcd") then | ||
428 | rb.splash(rb.HZ, sINITDATABASE) | ||
429 | os.exit(1); | ||
430 | end | ||
431 | if rb.cpu_boost then rb.cpu_boost(true) end | ||
432 | rb.backlight_force_on() | ||
433 | if not rb.dir_exists(playlistpath) then | ||
434 | luadir.mkdir(playlistpath) | ||
435 | end | ||
436 | rb.lcd_clear_display() | ||
437 | rb.lcd_update() | ||
438 | collectgarbage("collect") | ||
439 | create_random_playlist(rb.ROCKBOX_DIR .. "/database_4.tcd", | ||
440 | "random_playback.m3u8", max_tracks, play_on_success); | ||
441 | rb.splash(rb.HZ * 2, sGOODBYE) | ||
442 | -- Restore user backlight settings | ||
443 | rb.backlight_use_settings() | ||
444 | if rb.cpu_boost then rb.cpu_boost(false) end | ||
445 | |||
446 | --[[ | ||
447 | local used, allocd, free = rb.mem_stats() | ||
448 | local lu = collectgarbage("count") | ||
449 | local fmt = function(t, v) return string.format("%s: %d Kb\n", t, v /1024) end | ||
450 | |||
451 | -- this is how lua recommends to concat strings rather than .. | ||
452 | local s_t = {} | ||
453 | s_t[1] = "rockbox:\n" | ||
454 | s_t[2] = fmt("Used ", used) | ||
455 | s_t[3] = fmt("Allocd ", allocd) | ||
456 | s_t[4] = fmt("Free ", free) | ||
457 | s_t[5] = "\nlua:\n" | ||
458 | s_t[6] = fmt("Used", lu * 1024) | ||
459 | s_t[7] = "\n\nNote that the rockbox used count is a high watermark" | ||
460 | rb.splash_scroller(10 * rb.HZ, table.concat(s_t)) --]] | ||
461 | |||
462 | end --MAIN | ||
463 | |||
464 | main() -- BILGUS | ||