Hello,
I have tried recently to use the mtx-flac script on a part of my collection and found out that it's not working as expected. I tried to improve it just a bit, only to make it more robust. So here it is for everyone who may find it useful. I hope that Hans don't mind. There is also a hidden argument (--verbose) that spits out complete STREAMINFO and VORBIS COMMENT metadata blocks of every file.
You can see some demo in the attached files.

Best regards,
Ivan

If your question is of interest to others as well, please add an entry to the 
Wiki!

maillist : [email protected] / 
https://mailman.ntg.nl/mailman3/lists/ntg-context.ntg.nl
webpage  : https://www.pragma-ade.nl / https://context.aanhet.net (mirror)
archive  : https://github.com/contextgarden/context
wiki     : https://wiki.contextgarden.net
___________________________________________________________________________________
mtx-flac        | identifying files using pattern "c:\\Data\\context\\**.flac"
mtx-flac        | 4 files found, analyzing files
mtx-flac        | file c:/Data/context/Track_01.flac
STREAMINFO_block={
 ["bits_per_sample"]=16,
 ["maximum_block_size"]=4096,
 ["maximum_frame_size"]=13347,
 ["md5_signature"]="82c3f6b1ae58b2b7ca214a8c0572bf1f",
 ["minimum_block_size"]=4096,
 ["minimum_frame_size"]=3030,
 ["number_of_channels"]=2,
 ["sample_rate_in_hz"]=44100,
 ["samples_in_stream"]=16559761,
}
VORBIS_COMMENT_block={
 ["album"]="Distance Over Time (Bonus Track Version)",
 ["artist"]="Dream Theater",
 ["copyright"]="2019 Ytse Jams, Inc.",
 ["date"]="2018",
 ["genre"]="Progressive Metal",
 ["organization"]="Inside Out Music",
 ["replaygain_album_gain"]="-4.06 dB",
 ["replaygain_album_peak"]="1.027030",
 ["replaygain_track_gain"]="-4.37 dB",
 ["replaygain_track_peak"]="1.014315",
 ["title"]="Untethered Angel",
 ["tracknumber"]="1",
 ["tracktotal"]="10",
 ["vendor"]="reference libFLAC 1.3.2 20221022",
}
mtx-flac        | file c:/Data/context/Track_02.flac
STREAMINFO_block={
 ["bits_per_sample"]=16,
 ["maximum_block_size"]=4096,
 ["maximum_frame_size"]=13214,
 ["md5_signature"]="95c98854f0bbe576019615092bad10a6",
 ["minimum_block_size"]=4096,
 ["minimum_frame_size"]=2005,
 ["number_of_channels"]=2,
 ["sample_rate_in_hz"]=44100,
 ["samples_in_stream"]=11369024,
}
VORBIS_COMMENT_block={
 ["album"]="Distance Over Time (Bonus Track Version)",
 ["artist"]="Dream Theater",
 ["copyright"]="2019 Ytse Jams, Inc.",
 ["date"]="2018",
 ["genre"]="Progressive Metal",
 ["organization"]="Inside Out Music",
 ["replaygain_album_gain"]="-4.06 dB",
 ["replaygain_album_peak"]="1.027030",
 ["replaygain_track_gain"]="-4.18 dB",
 ["replaygain_track_peak"]="0.996791",
 ["title"]="Paralyzed",
 ["tracknumber"]="2",
 ["tracktotal"]="10",
 ["vendor"]="reference libFLAC 1.3.2 20221022",
}
mtx-flac        | file c:/Data/context/Track_03.flac
STREAMINFO_block={
 ["bits_per_sample"]=16,
 ["maximum_block_size"]=4096,
 ["maximum_frame_size"]=13341,
 ["md5_signature"]="2dce469b15b4e05a3dfc6f4365878620",
 ["minimum_block_size"]=4096,
 ["minimum_frame_size"]=1770,
 ["number_of_channels"]=2,
 ["sample_rate_in_hz"]=44100,
 ["samples_in_stream"]=18720862,
}
VORBIS_COMMENT_block={
 ["album"]="Distance Over Time (Bonus Track Version)",
 ["artist"]="Dream Theater",
 ["copyright"]="2019 Ytse Jams, Inc.",
 ["date"]="2018",
 ["genre"]="Progressive Metal",
 ["organization"]="Inside Out Music",
 ["replaygain_album_gain"]="-4.06 dB",
 ["replaygain_album_peak"]="1.027030",
 ["replaygain_track_gain"]="-3.87 dB",
 ["replaygain_track_peak"]="1.020386",
 ["title"]="Fall Into the Light",
 ["tracknumber"]="3",
 ["tracktotal"]="10",
 ["vendor"]="reference libFLAC 1.3.2 20221022",
}
mtx-flac        | file c:/Data/context/Track_04.flac
STREAMINFO_block={
 ["bits_per_sample"]=16,
 ["maximum_block_size"]=4096,
 ["maximum_frame_size"]=13334,
 ["md5_signature"]="810b9faa60d6dd49dc99f0f2d9540bff",
 ["minimum_block_size"]=4096,
 ["minimum_frame_size"]=1911,
 ["number_of_channels"]=2,
 ["sample_rate_in_hz"]=44100,
 ["samples_in_stream"]=17790998,
}
VORBIS_COMMENT_block={
 ["album"]="Distance Over Time (Bonus Track Version)",
 ["artist"]="Dream Theater",
 ["copyright"]="2019 Ytse Jams, Inc.",
 ["date"]="2018",
 ["genre"]="Progressive Metal",
 ["organization"]="Inside Out Music",
 ["replaygain_album_gain"]="-4.06 dB",
 ["replaygain_album_peak"]="1.027030",
 ["replaygain_track_gain"]="-4.40 dB",
 ["replaygain_track_peak"]="0.970766",
 ["title"]="Barstool Warrior",
 ["tracknumber"]="4",
 ["tracktotal"]="10",
 ["vendor"]="reference libFLAC 1.3.2 20221022",
}
mtx-flac        | saving data in file "music-collection.xml"
mtx-flac        | error in album: "Distance Over Time (Bonus Track Version)" of 
"Dream Theater", no track 5
mtx-flac        | error in album: "Distance Over Time (Bonus Track Version)" of 
"Dream Theater", no track 6
mtx-flac        | error in album: "Distance Over Time (Bonus Track Version)" of 
"Dream Theater", no track 7
mtx-flac        | error in album: "Distance Over Time (Bonus Track Version)" of 
"Dream Theater", no track 8
mtx-flac        | error in album: "Distance Over Time (Bonus Track Version)" of 
"Dream Theater", no track 9
mtx-flac        | error in album: "Distance Over Time (Bonus Track Version)" of 
"Dream Theater", no track 10
mtx-flac        | 4 tracks of 1 albums of 1 artists saved in 
"music-collection.xml" (6 errors)
<?xml version='1.0' standalone='yes'?>

<collection>
    <artist>
	<name>Dream Theater</name>
	<albums>
	    <album year='2018'>
		<name>Distance Over Time (Bonus Track Version)</name>
		<tracks>
		    <track track='1' length='376'>Untethered Angel</track>
		    <track track='2' length='258'>Paralyzed</track>
		    <track track='3' length='425'>Fall Into the Light</track>
		    <track track='4' length='403'>Barstool Warrior</track>
		    <error track='5'/>
		    <error track='6'/>
		    <error track='7'/>
		    <error track='8'/>
		    <error track='9'/>
		    <error track='10'/>
		</tracks>
	    </album>
	</albums>
    </artist>
</collection>
if not modules then modules = { } end modules ['mtx-flac'] = {
    version   = 1.001,
    comment   = "companion to mtxrun.lua",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

local sub, match, byte, lower = string.sub, string.match, string.byte, 
string.lower
local readstring, readnumber = io.readstring, io.readnumber
local concat, sortedpairs, sort, keys = table.concat, table.sortedpairs, 
table.sort, table.keys
local tonumber = tonumber
local tobitstring = number.tobitstring
local lpegmatch = lpeg.match
local p_escaped = lpeg.patterns.xml.escaped

-- rather silly: pack info in bits while a flac file is large anyway

flac = flac or { }

flac.report = string.format

local splitter = lpeg.splitat("=")
local readers  = { }

readers[0] = function(f,size,target) -- not yet ok .. todo: use bit32 lib
    local info = { }
    target.info = info
    info.minimum_block_size = readnumber(f,2)
    info.maximum_block_size = readnumber(f,2)
    info.minimum_frame_size = readnumber(f,3)
    info.maximum_frame_size = readnumber(f,3)
    local buffer = { }
    for i=1,8 do
        buffer[i] = tobitstring(readnumber(f,1),1,8)
    end
    local bytes = concat(buffer)
    info.sample_rate_in_hz  = tonumber(sub(bytes, 1,20),2) -- 20
    info.number_of_channels = tonumber(sub(bytes,21,23),2) + 1 --  3
    info.bits_per_sample    = tonumber(sub(bytes,24,28),2) + 1 --  5
    info.samples_in_stream  = tonumber(sub(bytes,29,64),2) -- 36
    local md5_readable = ""
    for i = 1, 16 do
        local b = string.format("%02x", readnumber(f, 1))
        md5_readable = md5_readable .. b
    end
    info.md5_signature = md5_readable
end

readers[4] = function(f,size,target,banner)
    local tags = { }
    target.tags = tags
    target.vendor = readstring(f,readnumber(f,-4))
    target.tags["vendor"] = target.vendor
    for i=1,readnumber(f,-4) do
        local key, value = lpeg.match(splitter,readstring(f,readnumber(f,-4)))
        tags[lower(key)] = value
    end
end

readers.default = function(f,size,target)
    f:seek("cur",size)
end

local valid = {
    ["fLaC"] = true,
    ["ID3♥"] = false,
}

function flac.getmetadata(filename)
    local f = io.open(filename, "rb")
    if f then
        local banner  = readstring(f,4)
        local whatsit = valid[banner]
        if whatsit ~= nil then
            if whatsit == false then
                flac.report("suspicious flac file: %s (%s)",filename,banner)
            end
            local data = {
                banner   = banner,
                filename = filename,
                filesize = lfs.attributes(filename,"size"),
            }
            while true do
                local flag = readnumber(f,1)
                local size = readnumber(f,3)
                local last = flag > 127
                if last then
                    flag = flag - 128
                end
                local reader = readers[flag] or readers.default
                reader(f,size,data)
                if last then
                    f:close()
                    return data
                end
            end
        else
            flac.report("no flac file: %s (%s)",filename,banner)
        end
        f:close()
    else
        flac.report("no file: %s",filename)
    end
end


function flac.savecollection(pattern,filename)
    pattern = (pattern ~= "" and pattern) or "**/*.flac"
    filename = (filename ~= "" and filename) or "music-collection.xml"
    flac.report("identifying files using pattern %q" ,pattern)
    local files = dir.glob(pattern)
    flac.report("%s files found, analyzing files",#files)
    local music = { }
    sort(files)
    for i=1,#files do
        local name = files[i]
        local data = flac.getmetadata(name)
        if data then
            local tags   = data.tags
            local info   = data.info
            -- hidden argument to print all metadata
            if environment.argument("verbose") then
                flac.report("file %s",name)
                table.print(info,"STREAMINFO_block")
                table.print(tags,"VORBIS_COMMENT_block")
            end
            if tags and info then
                local artist = tags.artist or "no-artist"
                local titl = tags.title or "no-title"
                local album  = tags.album  or "no-album"
                local albums = music[artist]
                if not albums then
                    albums = { }
                    music[artist] = albums
                end
                local albumx = albums[album]
                if not albumx then
                    albumx = {
                        year   = tags.date,
                        tracks = { },
                    }
                    albums[album] = albumx
                end
                albums[album].tracks[tonumber(tags.tracknumber) or 0] = {
                    tracknumber = tonumber(tags.tracknumber) or 0,
                    tracktotal = tonumber(tags.tracktotal) or 
tonumber(tags.totaltracks) or 0,
                    title  = titl,
                    length = 
math.round((info.samples_in_stream/info.sample_rate_in_hz)),
                }
            else
                flac.report("unable to read file",name)
            end
        end
    end
    --
    local nofartists = 0
    local nofalbums  = 0
    local noftracks  = 0
    local noferrors  = 0
    --
    local allalbums
    local function compare(a,b)
        local ya = allalbums[a].year or 0
        local yb = allalbums[b].year or 0
        if ya == yb then
            return a < b
        else
            return ya < yb
        end
    end
    local function getlist(albums)
        allalbums = albums
        local list = keys(albums)
        sort(list,compare)
        return list
    end
    --
    filename = file.addsuffix(filename,"xml")
    local f = io.open(filename,"wb")
    if f then
        flac.report("saving data in file %q",filename)
        f:write("<?xml version='1.0' standalone='yes'?>\n\n")
        f:write("<collection>\n")
        for artist, albums in sortedpairs(music) do
            nofartists = nofartists + 1
            f:write("    <artist>\n")
            f:write("\t<name>",lpegmatch(p_escaped,artist),"</name>\n")
            f:write("\t<albums>\n")
            local list = getlist(albums)
            nofalbums = nofalbums + #list
            for nofalbums=1,#list do
                local album = list[nofalbums]
                local data  = albums[album]
                f:write("\t    <album year='",data.year or 0,"'>\n")
                f:write("\t\t<name>",lpegmatch(p_escaped,album),"</name>\n")
                f:write("\t\t<tracks>\n")
                local tracks = data.tracks
                local totaltracks = tracks[keys(tracks)[1]].tracktotal
                if totaltracks ~= 0 then
                    for i = 1,totaltracks do
                        local track = tracks[i]
                        if track then
                            noftracks = noftracks + 1
                            f:write("\t\t    <track 
track='",track.tracknumber,"' 
length='",track.length,"'>",lpegmatch(p_escaped,track.title),"</track>\n")
                        else
                            noferrors = noferrors + 1
                            flac.report("error in album: %q of %q, no track 
%s",album,artist,i)
                            f:write("\t\t    <error track='",i,"'/>\n")
                        end
                    end
                else
                    noferrors = noferrors + 1
                    flac.report("error in album: unknown total number of tracks 
in album %q of %q",album,artist)
                    f:write("\t\t    <error>Unknown total number of 
tracks</error>\n")
                    for i, _ in pairs(tracks) do
                        local track = tracks[i]
                        if track then
                            noftracks = noftracks + 1
                            f:write("\t\t    <track 
track='",track.tracknumber,"' 
length='",track.length,"'>",lpegmatch(p_escaped,track.title),"</track>\n")
                        end
                    end
                end

                f:write("\t\t</tracks>\n")
                f:write("\t    </album>\n")
            end
            f:write("\t</albums>\n")
            f:write("    </artist>\n")
        end
        f:write("</collection>\n")
        f:close()
        flac.report("%s tracks of %s albums of %s artists saved in %q (%s 
errors)",noftracks,nofalbums,nofartists,filename,noferrors)
        -- a secret option for alan braslau
        if environment.argument("bibtex") then
            filename = file.replacesuffix(filename,"bib")
            local f = io.open(filename,"wb")
            if f then
                local n = 0
                for artist, albums in sortedpairs(music) do
                    local list = getlist(albums)
                    for nofalbums=1,#list do
                        n = n + 1
                        local album  = list[nofalbums]
                        local data   = albums[album]
                        local tracks = data.tracks
                        f:write("@cd{entry-",n,",\n")
                        f:write("\tartist   = {",artist,"},\n")
                        f:write("\ttitle    = {",album or "no title","},\n")
                        f:write("\tyear     = {",data.year or 0,"},\n")
                        f:write("\ttracks   = {",#tracks,"},\n")
                        for i=1,#tracks do
                            local track = tracks[i]
                            if track then
                                noftracks = noftracks + 1
                                f:write("\ttrack:",i,"  = {",track.title,"},\n")
                                f:write("\tlength:",i," = 
{",track.length,"},\n")
                            end
                        end
                        f:write("}\n")
                    end
                end
                f:close()
                flac.report("additional bibtex file generated: %s",filename)
            end
        end
        --
    else
        flac.report("unable to save data in file %q",filename)
    end
end

--

local helpinfo = [[
<?xml version="1.0"?>
<application>
 <metadata>
  <entry name="name">mtx-flac</entry>
  <entry name="detail">ConTeXt Flac Helpers</entry>
  <entry name="version">0.10</entry>
 </metadata>
 <flags>
  <category name="basic">
   <subcategory>
    <flag name="collect"><short>collect albums in xml file</short></flag>
    <flag name="pattern"><short>use pattern for locating files</short></flag>
   </subcategory>
  </category>
 </flags>
 <examples>
  <category>
   <title>Example</title>
   <subcategory>
    <example><command>mtxrun --script flac --collect 
somename.flac</command></example>
    <example><command>mtxrun --script flac --collect 
--pattern="m:/music/**")</command></example>
   </subcategory>
  </category>
 </examples>
</application>
]]

local application = logs.application {
    name     = "mtx-flac",
    banner   = "ConTeXt Flac Helpers 0.10",
    helpinfo = helpinfo,
}

flac.report = application.report

-- script code

scripts      = scripts      or { }
scripts.flac = scripts.flac or { }

function scripts.flac.collect()
    local files   = environment.files
    local pattern = environment.arguments.pattern
    if #files > 0 then
        for i=1,#files do
            local filename = files[1]
            if file.suffix(filename) == "flac" then
                flac.savecollection(filename,file.replacesuffix(filename,"xml"))
            elseif lfs.isdir(filename) then
                local pattern = filename .. "/**.flac"
                
flac.savecollection(pattern,file.addsuffix(file.basename(filename),"xml"))
            else
                
flac.savecollection(file.replacesuffix(filename,"flac"),file.replacesuffix(filename,"xml"))
            end
        end
    elseif pattern then
        
flac.savecollection(file.addsuffix(pattern,"flac"),"music-collection.xml")
    else
        flac.report("no file(s) or pattern given" )
    end
end

if environment.argument("collect") then
    scripts.flac.collect()
elseif environment.argument("exporthelp") then
    application.export(environment.argument("exporthelp"),environment.files[1])
else
    application.help()
end
___________________________________________________________________________________
If your question is of interest to others as well, please add an entry to the 
Wiki!

maillist : [email protected] / 
https://mailman.ntg.nl/mailman3/lists/ntg-context.ntg.nl
webpage  : https://www.pragma-ade.nl / https://context.aanhet.net (mirror)
archive  : https://github.com/contextgarden/context
wiki     : https://wiki.contextgarden.net
___________________________________________________________________________________

Reply via email to