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
___________________________________________________________________________________