Rockbox.org home
Downloads
Release release
Dev builds dev builds
Extras extras
themes themes
Documentation
Manual manual
Wiki wiki
Device Status device status
Support
Forums forums
Mailing lists mailing lists
IRC IRC
Development
Bugs bugs
Patches patches
Dev Guide dev guide
Search




Rockbox Technical Forums


Login with username, password and session length
Home Help Search Staff List Login Register
News:

Rockbox Ports are now being developed for various digital audio players!

+  Rockbox Technical Forums
|-+  Support and General Use
| |-+  Audio Playback, Database and Playlists
| | |-+  TagNav Customizer
« previous next »
  • Print
Pages: [1]

Author Topic: TagNav Customizer  (Read 1160 times)

Offline Bilgus

  • Developer
  • Member
  • *
  • Posts: 464
TagNav Customizer
« on: July 22, 2018, 11:30:03 AM »
I made a little sample tagnav customizer in lua it requires some functions from the Rlimage update so you'll need a
Dev version newer than todays (072318) I still have a few targets to fix before I commit these changes

So what does it do?
Allows you to make custom nav menus based on the content stored in your database (database_x.tcd) files

Its highly beta and somewhat rigid in nature but at least usable enough to spit out something you could
easily edit through the text editor on your device if needed or use it as the start of something truly spectacular

On to a little bit about how to use it
You have 5 fields (Artist, AlbumArtist, Album, Genre, Composer) you can select one or more of each and then combine them
using OR, AND you also have the choice of ! which means the field doesn't contain those items

That might be a bit confusing at first so I'll go a bit more in depth

The fields by default use '@' (is one of the items in the list: "item1|item2|item3")
If you choose '!, AND', '!, OR' the fields use instead '!~' (does not contain)
See the Database wiki for more info https://www.rockbox.org/wiki/DataBase

OR, AND are the combinations between fields
So Artist(......) AND Genre(....) OR Composer(.......)

Code: [Select]
-- tagnav.lua BILGUS 2018

package.path = "/LuaTagNav/?.lua;" .. package.path --add the LuaTagnav directory to path
require("printmenu") --menu
require("printtable")
require("dbgettags")


local _lcd = require("lcd")
local _print = require("print")


local sERROROPENING   = "Error opening"
local sERRORMENUENTRY = "Error finding menu entry"

local sBLANKLINE = "##sBLANKLINE##"
local sDEFAULTMENU = "customfilter"

local sFILEOUT    = "/.rockbox/tagnavi_custom.config"
local sFILEHEADER = "#! rockbox/tagbrowser/2.0"
local sMENUSTART  = "%menu_start \"custom\" \"Database\""
local sMENUTITLE  = "title = \"fmt_title\""

local TAG_ARTIST, TAG_ALBARTIST, TAG_ALBUM, TAG_GENRE, TAG_COMPOSER = 1, 2, 3, 4, 5
local ts_TAGTYPE = {"Artist", "AlbumArtist", "Album", "Genre", "Composer"}
local ts_DBPATH  = {"database_0.tcd", "database_7.tcd", "database_1.tcd", "database_2.tcd", "database_5.tcd"}

local COND_OR, COND_AND, COND_NOR, COND_NAND = 1, 2, 3, 4
local ts_CONDITIONALS = {"OR", "AND", "!, OR", "!, AND"}
local ts_CONDSYMBOLS    = {"|", "&", "|", "&"}

local ts_YESNO        = {"", "Yes", "No"}
local s_OVERWRITE     = "Overwrite"
local s_EXISTS        = "Exists"


local function question(tInquiry, start)
    settings = {}
    settings.justify = "center"
    settings.wrap = true
    settings.msel = false
    settings.hasheader = true
    settings.co_routine = nil
    settings.curpos = start or 1
    local sel = print_table(tInquiry, #tInquiry, settings)
    return sel
end

local function find_linepos(t_lines, search, startline)
    startline = startline or 1

    for i = startline, #t_lines do
        if string.match (t_lines[i], search) then
            return i
        end
    end

    return -1
end

local function replacelines(t_lines, search, replace, startline)
    startline = startline or 1
    repcount = 0
    for i = startline, #t_lines do
        if string.match (t_lines[i], search) then
            t_lines[i] = replace
            repcount = repcount + 1
        end
    end
    return repcount
end

local function replaceemptylines(t_lines, replace, startline)
    startline = startline or 1
    replace = replace or nil
    repcount = 0
    for i = startline, #t_lines do
        if t_lines[i] == "" then
            t_lines[i] = replace
            repcount = repcount + 1
        end
    end
    return repcount
end

local function checkexistingmenu(t_lines, menuname)
    local pos = find_linepos(t_lines, "^\"" .. menuname .. "\"%s*%->.+")
    local sel = 0
    if pos > 0 then
        ts_YESNO[1] = menuname .. " " .. s_EXISTS .. ", " ..  s_OVERWRITE .. "?"
        sel = question(ts_YESNO, 3)
        if sel == 3 then
            pos = nil
        elseif sel < 2 then
            pos = 0
        end
    else
        pos = nil
    end
    return pos
end

local function savedata(filename, ts_tags, cond, menuname)
        menuname = menuname or sDEFAULTMENU

        local lines = {}
        local curline = 0
        local function lines_next(str, pos)
            pos = pos or #lines + 1
            lines[pos] = str or ""
            curline = pos
        end

        local function lines_append(str, pos)
            pos = pos or curline or #lines
            lines[pos] = lines[pos] .. str or ""
        end

        if rb.file_exists(filename) ~= true then
            lines_next(sFILEHEADER)
            lines_next("#")
            lines_next("# MAIN MENU")
            lines_next(sMENUSTART)
        else
            local file = io.open(filename, "r") -- read
            if not file then
                rb.splash(rb.HZ, "Error opening" .. " " .. filename)
                return
            end

            for line in file:lines() do
                lines_next(line)
            end
            file:close()
        end

        local menupos = find_linepos(lines, sMENUSTART)
        if menupos < 1 then
            rb.splash(rb.HZ, sERRORMENUENTRY)
            return
        end

        replaceemptylines(lines, sBLANKLINE, menupos)

        local existmenupos = checkexistingmenu(lines, menuname)
        if existmenupos and existmenupos < 1 then return end -- user canceled

        local lastcond = ""
        local n_cond = COND_OR
        local tags, tagtype

        local function buildtag(e_tagtype)
            if ts_tags[e_tagtype] then
                lines_append(lastcond)
                n_cond = (cond[e_tagtype] or COND_OR)

                tags = ts_tags[e_tagtype]
                tagtype = string.lower(ts_TAGTYPE[e_tagtype])

                if n_cond <= COND_AND then
                    lines_append(" " .. tagtype)
                    lines_append(" @ \"".. table.concat(tags, " | ")  .. "\"")
                else
                    for k = 1, #tags do
                        lines_append(" " .. tagtype)
                        lines_append(" !~ \"".. tags[k] .. "\"")
                        if k < #tags then lines_append(" &") end
                    end
                end
                lastcond = " " .. ts_CONDSYMBOLS[n_cond]
            end       
        end

        if ts_tags[TAG_ARTIST] or ts_tags[TAG_ALBARTIST] or ts_tags[TAG_ALBUM] or
           ts_tags[TAG_GENRE] or ts_tags[TAG_COMPOSER] then

            lines_next("\"" .. menuname .. "\" -> " .. sMENUTITLE .. " ?", existmenupos)

            buildtag(TAG_ARTIST)
            buildtag(TAG_ALBARTIST)
            buildtag(TAG_ALBUM)
            buildtag(TAG_GENRE)
            buildtag(TAG_COMPOSER)

            lines_next("\n")
        else
            rb.splash(rb.HZ, "Nothing to save")
        end

        local file = io.open(filename, "w+") -- overwrite
        if not file then
            rb.splash(rb.HZ, "Error writing " .. filename)
            return
        end

        for i = 1, #lines do
            if lines[i] and lines[i] ~= sBLANKLINE then
                file:write(lines[i], "\n")
            end
        end

        file:close()
end

-- uses print_table to display database tags
local function print_tags(ftable, settings, t_selected)
    if not s_cond then s_sep = "|" end
    ftable = ftable or {}

    if t_selected then
        for k = 1, #t_selected do
            ftable[t_selected[k]] = ftable[t_selected[k]] .. "\0"
        end
    end
    _lcd:clear()
    _print.clear()

    if not settings then
        settings = {}
        settings.justify = "center"
        settings.wrap = true
        settings.msel = true
    end

    settings.hasheader = true
    settings.co_routine = nil

    local sel = print_table(ftable, #ftable, settings)

    --_lcd:splashf(rb.HZ * 2, "%d items {%s}", #sel, table.concat(sel, ", "))
    local selected = {}
    local str = ""
    for k = 1,#sel do
        str = ftable[sel[k]] or ""
        selected[#selected + 1] = string.sub(str, 1, -2) -- REMOVE \0
    end

    ftable = nil

    if #sel == 0 then
        return nil, nil
    end

    return sel, selected
end -- print_tags

-- uses print_table to display a menu
function main_menu()
    local menuname = sDEFAULTMENU
    local t_tags
    local ts_tags = {}
    local cond = {}
    local sel = {}
    local mt =  {
                [1] = "TagNav Customizer",
                [2] = ts_TAGTYPE[TAG_ARTIST],
                [3] = ts_CONDITIONALS[cond[TAG_ARTIST] or COND_OR],
                [4] = ts_TAGTYPE[TAG_ALBARTIST],
                [5] = ts_CONDITIONALS[cond[TAG_ALBARTIST] or COND_OR],
                [6] = ts_TAGTYPE[TAG_ALBUM],
                [7] = ts_CONDITIONALS[cond[TAG_ALBUM] or COND_OR],
                [8] = ts_TAGTYPE[TAG_GENRE],
                [9] = ts_CONDITIONALS[cond[TAG_GENRE] or COND_OR],
                [10] = ts_TAGTYPE[TAG_COMPOSER],
                [11] = "",
                [12] = "Save to Tagnav",
                [13] = "Exit"
                }

    local function sel_cond(item, item_mt)
        cond[item] = cond[item] or 1
        cond[item] = cond[item] + 1
        if cond[item] > #ts_CONDITIONALS then cond[item] = 1 end
        mt[item_mt] = ts_CONDITIONALS[cond[item]]
    end

    local function sel_tag(item, item_mt, t_tags)
        t_tags = get_tags(rb.ROCKBOX_DIR .. "/" .. ts_DBPATH[item], ts_TAGTYPE[item])
        sel[item], ts_tags[item] = print_tags(t_tags, nil, sel[item])
        if ts_tags[item] then
            mt[item_mt] = ts_TAGTYPE[item] .. " [" .. #sel[item] .. "]"
        else
            mt[item_mt] = ts_TAGTYPE[item]
        end
    end

    local ft =  {
                [0] = exit_now, --if user cancels do this function
                [1] = function(TITLE) return true end, -- shouldn't happen title occupies this slot
                [2]  = function(ART)
                            sel_tag(TAG_ARTIST, ART, t_tags)
                        end,
                [3]  = function(ARTCOND)
                            sel_cond(TAG_ARTIST, ARTCOND)
                        end,
                [4]  = function(ALBART)
                            sel_tag(TAG_ALBARTIST, ALBART, t_tags)                           
                        end,
                [5]  = function(ALBARTCOND)
                            sel_cond(TAG_ALBARTIST, ALBARTCOND)
                        end,
                [6]  = function(ALB)
                            sel_tag(TAG_ALBUM, ALB, t_tags)
                        end,
                [7]  = function(ALBCOND)
                            sel_cond(TAG_ALBUM, ALBCOND)
                        end,
                [8]  = function(GENRE)
                            sel_tag(TAG_GENRE, GENRE, t_tags)                           
                        end,
                [9]  = function(GENRECOND)
                            sel_cond(TAG_GENRE, GENRECOND)
                        end,
                [10]  = function(COMP)
                            sel_tag(TAG_COMPOSER, COMP, t_tags)
                        end,
                [11]  = function(COMPCOND)
                            sel_cond(TAG_COMPOSER, COMPCOND)
                        end,
                [12]  = function(SAVET)
                            menuname = menuname or sDEFAULTMENU
                            menuname = rb.kbd_input(menuname)
                            menuname = string.match(menuname, "%w+")
                            if menuname == "" then menuname = nil end
                            menuname = menuname or sDEFAULTMENU
                            savedata(sFILEOUT, ts_tags, cond, menuname)
                        end,
                [13] = function(EXIT_) return true end
                }

    print_menu(mt, ft)

end

function exit_now()
    _lcd:update()
    os.exit()
end -- exit_now

main_menu()
exit_now()

Code: [Select]
--dbgettags.lua Bilgus 2018
--[[
/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2017 William Wilgus
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
]]

require("actions")
local CANCEL_BUTTON = rb.actions.PLA_CANCEL

local sINVALIDDATABASE = "Invalid Database"
local sERROROPENING    = "Error opening"

-- tag cache header
local sTCVERSION = string.char(0x0F)
local sTCHEADER  = string.reverse("TCH" .. sTCVERSION)
local DATASZ    = 4  -- int32_t
local TCHSIZE   = 3 * DATASZ -- 3 x int32_t

local function bytesLE_n(str)
    str = str or ""
    local tbyte={str:byte(1, -1)}
    local bpos = 1
    local num  = 0
    for k = 1,#tbyte do -- (k = #t, 1, -1 for BE)
        num = num + tbyte[k] * bpos
        bpos = bpos * 256
    end
    return num
end

-- uses database files to retrieve database tags
-- adds all unique tags into a lua table
function get_tags(filename, hstr)

    if not filename then return end

    hstr = hstr or filename

    local file = io.open('/' .. filename or "", "r") --read
    if not file then rb.splash(100, sERROROPENING .. " " ..  filename) return end

    local fsz = file:seek("end")

    local posln = 0
    local tag_len = TCHSIZE
    local idx

    local function readchrs(count)
        if posln >= fsz then return nil end
        file:seek("set", posln)
        posln = posln + count
        return file:read(count)
    end

    local tagcache_header = readchrs(DATASZ) or ""
    local tagcache_sz = readchrs(DATASZ) or ""
    local tagcache_entries = readchrs(DATASZ) or ""

    if tagcache_header ~= sTCHEADER or
        bytesLE_n(tagcache_sz) ~= (fsz - TCHSIZE) then
        rb.splash(100, sINVALIDDATABASE .. " " .. filename)
        return
    end
   
    -- local tag_entries = bytesLE_n(tagcache_entries)

    local ftable = {}
    table.insert(ftable, 1, hstr)

    local tline = #ftable + 1
    ftable[tline] = ""

    local str = ""

    while true do
        tag_len = bytesLE_n(readchrs(DATASZ))
        readchrs(DATASZ) -- idx = bytesLE_n(readchrs(DATASZ))
        str = readchrs(tag_len) or ""
        str = string.match(str, "(%Z+)%z")

        if str then
            if ftable[tline - 1] ~= str then -- Remove dupes
                ftable[tline] = str
                tline = tline + 1
            end
        elseif posln >= fsz then
            break
        end

        if rb.get_plugin_action(0) == CANCEL_BUTTON then
            break
        end
    end

    file:close()

    return ftable
end -- get_tags

Here are all the files you'll need..
http://www.mediafire.com/?4emie2cz23b5x in the plugin/demo directory under lua_scripts

* TagNav.png (25.51 kB, 957x409 - viewed 173 times.)
« Last Edit: July 29, 2019, 10:04:15 PM by Bilgus »
Logged

Offline saratoga

  • Developer
  • Member
  • *
  • Posts: 8770
Re: TagNav Customizer
« Reply #1 on: July 23, 2018, 05:34:06 AM »
Really neat!
Logged

Offline Bilgus

  • Developer
  • Member
  • *
  • Posts: 464
Re: TagNav Customizer
« Reply #2 on: July 23, 2018, 07:05:13 AM »
Thanks, Saratoga Someone had mentioned an off device app doing this and I thought it an intriguing idea.

The only hangup might be people with really large databases, their device might not have enough ram to actually display the whole tag list.
I made it build each list on the fly and only save selections for this exact reason but in that case I might have to come up with a different tactic
like storing them to an intermediary file and use the incremental functionality from printtable.lua instead.

Beyond that remember you have to restart your player for the new entries to show in the TagNav list.
Logged

Offline Bilgus

  • Developer
  • Member
  • *
  • Posts: 464
Re: TagNav Customizer
« Reply #3 on: August 01, 2019, 10:44:47 PM »
TagNav Customizer can now be found in Plugins/demos/lua_scripts

you'll only need to restart after the first entry is added after that choose the 'Reload...' item added to the custom menu
Logged

Offline Duceboia

  • Member
  • *
  • Posts: 24
Re: TagNav Customizer
« Reply #4 on: August 14, 2019, 10:35:08 AM »
What a nice feature, thanks Bilgus!
 :)
Logged

Offline Bilgus

  • Developer
  • Member
  • *
  • Posts: 464
Re: TagNav Customizer
« Reply #5 on: August 14, 2019, 11:01:14 AM »
Duceboia, you are welcome!
Logged

  • Print
Pages: [1]
« previous next »
+  Rockbox Technical Forums
|-+  Support and General Use
| |-+  Audio Playback, Database and Playlists
| | |-+  TagNav Customizer
 

  • SMF 2.0.6 | SMF © 2013, Simple Machines
  • XHTML
  • RSS
  • WAP2

Page created in 0.086 seconds with 45 queries.