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
18
19flac = flac or { }
20
21flac.report = string.format
22
23local splitter = lpeg.splitat("=")
24local readers = { }
25
26readers[0] = function(f,size,target)
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)
39 info.number_of_channels = tonumber(sub(bytes,21,23),2)
40 info.bits_per_sample = tonumber(sub(bytes,24,28),2)
41 info.samples_in_stream = tonumber(sub(bytes,29,64),2)
42 info.md5_signature = readstring(f,16)
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
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
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 |