summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/plugins/lua_scripts/dbgettags.lua15
-rw-r--r--apps/plugins/lua_scripts/random_playlist.lua464
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
28local sINVALIDDATABASE = "Invalid Database" 28local sINVALIDDATABASE = "Invalid Database"
29local sERROROPENING = "Error opening" 29local sERROROPENING = "Error opening"
30 30
31-- tag cache header 31-- tag cache header
32local sTCVERSION = string.char(0x0F) 32sTCVERSION = string.char(0x10)
33local sTCHEADER = string.reverse("TCH" .. sTCVERSION) 33sTCHEADER = string.reverse("TCH" .. sTCVERSION)
34local DATASZ = 4 -- int32_t 34DATASZ = 4 -- int32_t
35local TCHSIZE = 3 * DATASZ -- 3 x int32_t 35TCHSIZE = 3 * DATASZ -- 3 x int32_t
36 36
37local function bytesLE_n(str) 37-- Converts array of bytes to proper endian
38function 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]]
23require ("actions")
24require("dbgettags")
25get_tags = nil -- unneeded
26
27-- User defaults
28local playlistpath = "/Playlists"
29local max_tracks = 500; -- size of playlist to create
30local min_repeat = 500; -- this many songs before a repeat
31local play_on_success = true;
32
33-- Random integer function
34local random = math.random; -- ref random(min, max)
35math.randomseed(rb.current_tick()); -- some kind of randomness
36
37-- Button definitions
38local CANCEL_BUTTON = rb.actions.PLA_CANCEL
39local OK_BUTTON = rb.actions.PLA_SELECT
40local ADD_BUTTON = rb.actions.PLA_UP
41local ADD_BUTTON_RPT = rb.actions.PLA_UP_REPEAT or ADD_BUTTON
42local SUB_BUTTON = rb.actions.PLA_DOWN
43local SUB_BUTTON_RPT = rb.actions.PLA_DOWN_REPEAT or SUB_BUTTON
44-- remove action and context tables to free some ram
45rb.actions = nil
46rb.contexts = nil
47-- Program strings
48local sINITDATABASE = "Initialize Database"
49local sHEADERTEXT = "Random Playlist"
50local sPLAYLISTERROR = "Playlist Error!"
51local sREMOVEPLAYLIST = "Removing Dynamic Playlist"
52local sSEARCHINGFILES = "Searching for Files.."
53local sERROROPENFMT = "Error Opening %s"
54local sINVALIDDBFMT = "Invalid Database %s"
55local sPROGRESSHDRFMT = "%d \\ %d Tracks"
56local sGOODBYE = "Goodbye"
57
58-- Gets size of text
59local function text_extent(msg, font)
60 font = font or rb.FONT_UI
61 return rb.font_getstringsize(msg, font)
62end
63
64local 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;
175end
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]]
180function 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
424end -- create_playlist
425
426local 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--[[
447local used, allocd, free = rb.mem_stats()
448local lu = collectgarbage("count")
449local 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 ..
452local s_t = {}
453s_t[1] = "rockbox:\n"
454s_t[2] = fmt("Used ", used)
455s_t[3] = fmt("Allocd ", allocd)
456s_t[4] = fmt("Free ", free)
457s_t[5] = "\nlua:\n"
458s_t[6] = fmt("Used", lu * 1024)
459s_t[7] = "\n\nNote that the rockbox used count is a high watermark"
460rb.splash_scroller(10 * rb.HZ, table.concat(s_t)) --]]
461
462end --MAIN
463
464main() -- BILGUS