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)) 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) -- 3 info.bits_per_sample = tonumber(sub(bytes,24,28),2) -- 5 info.samples_in_stream = tonumber(sub(bytes,29,64),2) -- 36 info.md5_signature = readstring(f,16) -- 128 end readers[4] = function(f,size,target,banner) local tags = { } target.tags = tags target.vendor = readstring(f,readnumber(f,-4)) 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,banner) 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 if tags and info then local artist = tags.artist or "no-artist" 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 albumx.tracks[tonumber(tags.tracknumber) or 0] = { title = tags.title, 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("\n\n") f:write("\n") for artist, albums in sortedpairs(music) do nofartists = nofartists + 1 f:write("\t\n") f:write("\t\t",lpegmatch(p_escaped,artist),"\n") f:write("\t\t\n") local list = getlist(albums) nofalbums = nofalbums + #list for nofalbums=1,#list do local album = list[nofalbums] local data = albums[album] f:write("\t\t\t\n") f:write("\t\t\t\t",lpegmatch(p_escaped,album),"\n") f:write("\t\t\t\t\n") local tracks = data.tracks for i=1,#tracks do local track = tracks[i] if track then noftracks = noftracks + 1 f:write("\t\t\t\t\t",lpegmatch(p_escaped,track.title),"\n") else noferrors = noferrors + 1 flac.report("error in album: %q of %q, no track %s",album,artist,i) f:write("\t\t\t\t\t\n") end end f:write("\t\t\t\t\n") f:write("\t\t\t\n") end f:write("\t\t\n") f:write("\t\n") end f:write("\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 = [[ mtx-flac ConTeXt Flac Helpers 0.10 collect albums in xml file use pattern for locating files Example mtxrun --script flac --collect somename.flac mtxrun --script flac --collect --pattern="m:/music/**") ]] 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