mtx-flac.lua /size: 10 Kb    last modification: 2020-07-01 14:35
1if not modules then modules = { } end modules ['mtx-flac'] = {
2    version   = 1.001,
3    comment   = "companion to mtxrun.lua",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9local sub, match, byte, lower = string.sub, string.match, string.byte, string.lower
10local readstring, readnumber = io.readstring, io.readnumber
11local concat, sortedpairs, sort, keys = table.concat, table.sortedpairs, table.sort, table.keys
12local tonumber = tonumber
13local tobitstring = number.tobitstring
14local lpegmatch = lpeg.match
15local p_escaped = lpeg.patterns.xml.escaped
16
17-- rather silly: pack info in bits while a flac file is large anyway
18
19flac = flac or { }
20
21flac.report = string.format
22
23local splitter = lpeg.splitat("=")
24local readers  = { }
25
26readers[0] = function(f,size,target) -- not yet ok .. todo: use bit32 lib
27    local info = { }
28    target.info = info
29    info.minimum_block_size = readnumber(f,-2)
30    info.maximum_block_size = readnumber(f,-2)
31    info.minimum_frame_size = readnumber(f,-3)
32    info.maximum_frame_size = readnumber(f,-3)
33    local buffer = { }
34    for i=1,8 do
35        buffer[i] = tobitstring(readnumber(f,1))
36    end
37    local bytes = concat(buffer)
38    info.sample_rate_in_hz  = tonumber(sub(bytes, 1,20),2) -- 20
39    info.number_of_channels = tonumber(sub(bytes,21,23),2) --  3
40    info.bits_per_sample    = tonumber(sub(bytes,24,28),2) --  5
41    info.samples_in_stream  = tonumber(sub(bytes,29,64),2) -- 36
42    info.md5_signature = readstring(f,16) -- 128
43end
44
45readers[4] = function(f,size,target,banner)
46    local tags = { }
47    target.tags = tags
48    target.vendor = readstring(f,readnumber(f,-4))
49    for i=1,readnumber(f,-4) do
50        local key, value = lpeg.match(splitter,readstring(f,readnumber(f,-4)))
51        tags[lower(key)] = value
52    end
53end
54
55readers.default = function(f,size,target)
56    f:seek("cur",size)
57end
58
59local valid = {
60    ["fLaC"] = true,
61    ["ID3♥"] = false,
62}
63
64function flac.getmetadata(filename)
65    local f = io.open(filename, "rb")
66    if f then
67        local banner  = readstring(f,4)
68        local whatsit = valid[banner]
69        if whatsit ~= nil then
70            if whatsit == false then
71                flac.report("suspicious flac file: %s (%s)",filename,banner)
72            end
73            local data = {
74                banner   = banner,
75                filename = filename,
76                filesize = lfs.attributes(filename,"size"),
77            }
78            while true do
79                local flag = readnumber(f,1)
80                local size = readnumber(f,3)
81                local last = flag > 127
82                if last then
83                    flag = flag - 128
84                end
85                local reader = readers[flag] or readers.default
86                reader(f,size,data,banner)
87                if last then
88                    f:close()
89                    return data
90                end
91            end
92        else
93            flac.report("no flac file: %s (%s)",filename,banner)
94        end
95        f:close()
96    else
97        flac.report("no file: %s",filename)
98    end
99end
100
101function flac.savecollection(pattern,filename)
102    pattern = (pattern ~= "" and pattern) or "**/*.flac"
103    filename = (filename ~= "" and filename) or "music-collection.xml"
104    flac.report("identifying files using pattern %q" ,pattern)
105    local files = dir.glob(pattern)
106    flac.report("%s files found, analyzing files",#files)
107    local music = { }
108    sort(files)
109    for i=1,#files do
110        local name = files[i]
111        local data = flac.getmetadata(name)
112        if data then
113            local tags   = data.tags
114            local info   = data.info
115            if tags and info then
116                local artist = tags.artist or "no-artist"
117                local album  = tags.album  or "no-album"
118                local albums = music[artist]
119                if not albums then
120                    albums = { }
121                    music[artist] = albums
122                end
123                local albumx = albums[album]
124                if not albumx then
125                    albumx = {
126                        year   = tags.date,
127                        tracks = { },
128                    }
129                    albums[album] = albumx
130                end
131                albumx.tracks[tonumber(tags.tracknumber) or 0] = {
132                    title  = tags.title,
133                    length = math.round((info.samples_in_stream/info.sample_rate_in_hz)),
134                }
135            else
136                flac.report("unable to read file",name)
137            end
138        end
139    end
140    --
141    local nofartists = 0
142    local nofalbums  = 0
143    local noftracks  = 0
144    local noferrors  = 0
145    --
146    local allalbums
147    local function compare(a,b)
148        local ya = allalbums[a].year or 0
149        local yb = allalbums[b].year or 0
150        if ya == yb then
151            return a < b
152        else
153            return ya < yb
154        end
155    end
156    local function getlist(albums)
157        allalbums = albums
158        local list = keys(albums)
159        sort(list,compare)
160        return list
161    end
162    --
163    filename = file.addsuffix(filename,"xml")
164    local f = io.open(filename,"wb")
165    if f then
166        flac.report("saving data in file %q",filename)
167        f:write("<?xml version='1.0' standalone='yes'?>\n\n")
168        f:write("<collection>\n")
169        for artist, albums in sortedpairs(music) do
170            nofartists = nofartists + 1
171            f:write("\t<artist>\n")
172            f:write("\t\t<name>",lpegmatch(p_escaped,artist),"</name>\n")
173            f:write("\t\t<albums>\n")
174            local list = getlist(albums)
175            nofalbums = nofalbums + #list
176            for nofalbums=1,#list do
177                local album = list[nofalbums]
178                local data  = albums[album]
179                f:write("\t\t\t<album year='",data.year or 0,"'>\n")
180                f:write("\t\t\t\t<name>",lpegmatch(p_escaped,album),"</name>\n")
181                f:write("\t\t\t\t<tracks>\n")
182                local tracks = data.tracks
183                for i=1,#tracks do
184                    local track = tracks[i]
185                    if track then
186                        noftracks = noftracks + 1
187                        f:write("\t\t\t\t\t<track length='",track.length,"'>",lpegmatch(p_escaped,track.title),"</track>\n")
188                    else
189                        noferrors = noferrors + 1
190                        flac.report("error in album: %q of %q, no track %s",album,artist,i)
191                        f:write("\t\t\t\t\t<error track='",i,"'/>\n")
192                    end
193                end
194                f:write("\t\t\t\t</tracks>\n")
195                f:write("\t\t\t</album>\n")
196            end
197            f:write("\t\t</albums>\n")
198            f:write("\t</artist>\n")
199        end
200        f:write("</collection>\n")
201        f:close()
202        flac.report("%s tracks of %s albums of %s artists saved in %q (%s errors)",noftracks,nofalbums,nofartists,filename,noferrors)
203        -- a secret option for alan braslau
204        if environment.argument("bibtex") then
205            filename = file.replacesuffix(filename,"bib")
206            local f = io.open(filename,"wb")
207            if f then
208                local n = 0
209                for artist, albums in sortedpairs(music) do
210                    local list = getlist(albums)
211                    for nofalbums=1,#list do
212                        n = n + 1
213                        local album  = list[nofalbums]
214                        local data   = albums[album]
215                        local tracks = data.tracks
216                        f:write("@cd{entry-",n,",\n")
217                        f:write("\tartist   = {",artist,"},\n")
218                        f:write("\ttitle    = {",album or "no title","},\n")
219                        f:write("\tyear     = {",data.year or 0,"},\n")
220                        f:write("\ttracks   = {",#tracks,"},\n")
221                        for i=1,#tracks do
222                            local track = tracks[i]
223                            if track then
224                                noftracks = noftracks + 1
225                                f:write("\ttrack:",i,"  = {",track.title,"},\n")
226                                f:write("\tlength:",i," = {",track.length,"},\n")
227                            end
228                        end
229                        f:write("}\n")
230                    end
231                end
232                f:close()
233                flac.report("additional bibtex file generated: %s",filename)
234            end
235        end
236        --
237    else
238        flac.report("unable to save data in file %q",filename)
239    end
240end
241
242--
243
244local helpinfo = [[
245<?xml version="1.0"?>
246<application>
247 <metadata>
248  <entry name="name">mtx-flac</entry>
249  <entry name="detail">ConTeXt Flac Helpers</entry>
250  <entry name="version">0.10</entry>
251 </metadata>
252 <flags>
253  <category name="basic">
254   <subcategory>
255    <flag name="collect"><short>collect albums in xml file</short></flag>
256    <flag name="pattern"><short>use pattern for locating files</short></flag>
257   </subcategory>
258  </category>
259 </flags>
260 <examples>
261  <category>
262   <title>Example</title>
263   <subcategory>
264    <example><command>mtxrun --script flac --collect somename.flac</command></example>
265    <example><command>mtxrun --script flac --collect --pattern="m:/music/**")</command></example>
266   </subcategory>
267  </category>
268 </examples>
269</application>
270]]
271
272local application = logs.application {
273    name     = "mtx-flac",
274    banner   = "ConTeXt Flac Helpers 0.10",
275    helpinfo = helpinfo,
276}
277
278flac.report = application.report
279
280-- script code
281
282scripts      = scripts      or { }
283scripts.flac = scripts.flac or { }
284
285function scripts.flac.collect()
286    local files   = environment.files
287    local pattern = environment.arguments.pattern
288    if #files > 0 then
289        for i=1,#files do
290            local filename = files[1]
291            if file.suffix(filename) == "flac" then
292                flac.savecollection(filename,file.replacesuffix(filename,"xml"))
293            elseif lfs.isdir(filename) then
294                local pattern = filename .. "/**.flac"
295                flac.savecollection(pattern,file.addsuffix(file.basename(filename),"xml"))
296            else
297                flac.savecollection(file.replacesuffix(filename,"flac"),file.replacesuffix(filename,"xml"))
298            end
299        end
300    elseif pattern then
301        flac.savecollection(file.addsuffix(pattern,"flac"),"music-collection.xml")
302    else
303        flac.report("no file(s) or pattern given" )
304    end
305end
306
307if environment.argument("collect") then
308    scripts.flac.collect()
309elseif environment.argument("exporthelp") then
310    application.export(environment.argument("exporthelp"),environment.files[1])
311else
312    application.help()
313end
314