-n, --dry-run option
[mob/metatag] / metatag_engines.lua
1 -- metatag     Metadata extraction utility
2 -- Copyright (C) 2014  Christian Th├Ąter <ct@pipapo.org>
3 --
4 -- This program is free software: you can redistribute it and/or modify
5 -- it under the terms of the GNU General Public License as published by
6 -- the Free Software Foundation, either version 3 of the License, or
7 -- (at your option) any later version.
8 --
9 -- This program is distributed in the hope that it will be useful,
10 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
11 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 -- GNU General Public License for more details.
13 --
14 -- You should have received a copy of the GNU General Public License
15 -- along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 --
18 -- Engines
19 --
20
21
22 DOC.engines = DOC.engines.."\n"..[[
23  stop
24   Clears all found metadata, aborts processing the current file.
25   Called by exclude rules and for directories.
26 ]]
27 Meta:Engine (
28    "stop",
29    function (m,f)
30       dmsg("  stop: "..shquote(f))
31       m:Stop ()
32       return "break"
33    end
34 )
35
36
37 DOC.engines = DOC.engines.."\n"..[[
38  recurse_directory
39   Iterates over all members of a directory and processes them.
40 ]]
41 Meta:Engine (
42    "recurse_directory",
43    function (m,f)
44       for file in nixio.fs.dir(f) do
45          Meta:Process(f.."/"..file)
46       end
47    end
48 )
49
50
51 DOC.engines = DOC.engines.."\n"..[[
52  stat
53   calls the stat() function over the file add this data as `stat.<name>=value` metadata.
54   When the file is a symlink, then an additionall `stat.link=` will contain the resolved
55   filename (even over multiple symlinks). For symlinks which point to nothing (or in circles)
56   stat will return nothing.
57
58   Called by the very first rule which matches the initial '/=file' metadata.
59   Further basic processing rules (regular files, recurse directories, ...) are triggered on
60   this stat data.
61 ]]
62 Meta:Engine (
63    "stat",
64    function (m,f)
65       local stat = nixio.fs.stat (f)
66       if stat then
67          local litr = nixio.fs.readlink (f)
68          local link
69          while litr do
70             link = litr
71             litr = nixio.fs.readlink (litr)
72          end
73
74          if link then
75             stat.link = link
76          end
77
78          for k,v in pairs(stat) do
79             m:Op("stat."..k.."="..v)
80          end
81       end
82    end
83 )
84
85
86
87 DOC.engines = DOC.engines.."\n"..[[
88  mime_type
89   Find out the mime type of the given file, adds a `mime=<type>`.
90   Needs xdg-utils installed.
91 ]]
92 if shexists("xdg-mime") then
93    Meta:Engine (
94       "mime_type",
95       function (m,f)
96          m:Op("mime+="..string.match(sh("xdg-mime query filetype "..shquote(f)), "(.*)\n"))
97       end
98    )
99 else
100    msg("xdg-mime not found")
101 end
102
103 DOC.engines = DOC.engines.."\n"..[[
104  set
105   sets metadata as given in its colon separated options list.
106 ]]
107 Meta:Engine (
108    "set",
109    function (m, f, opts)
110       for op in opts:gmatch("([^:]*=[^:]*):?") do
111          m:Op(op)
112       end
113       return "multi"
114    end
115 )
116
117
118 DOC.engines = DOC.engines.."\n"..[[
119  mime_encoding
120   Find out the encoding of the given file, adds a `encoding=<type>` metadata.
121   Called on text files.
122 ]]
123 if shexists("file") then
124    Meta:Engine (
125       "mime_encoding",
126       function (m, f)
127          m:Op("encoding+="..string.match(sh("file --mime-encoding -b -L "..shquote(f)), "(.*)\n"))
128       end
129    )
130 end
131
132 DOC.engines = DOC.engines.."\n"..[[
133  wc
134   Runs the `wc` utility over the file. Adds `lines=`, `words=` and `characters=` metadata.
135   Called on text files.
136
137   options:
138   'lines', 'words' or 'characters' to export the respective parts only.
139 ]]
140 Meta:Engine (
141    "wc",
142    function (m, f, opts)
143       local lines, words, characters = string.match(sh("wc "..shquote(f)), "%s*(%d+)%s+(%d+)%s+(%d+)")
144       if opts == '' or opts:match("lines") then m:Op("lines="..lines) end
145       if opts == '' or opts:match("words") then m:Op("words="..words) end
146       if opts == '' or opts:match("characters") then m:Op("characters="..characters) end
147    end
148 )
149
150
151 DOC.engines = DOC.engines.."\n"..[[
152  time_to_secs
153   converts time from "hour:minute:seconds.secondfaction" format into
154   "seconds.secondfaction"
155 ]]
156 Meta:Engine (
157    "time_to_secs",
158    function (m, f, opts, k, v)
159       local hours,minutes,seconds,frac = v:match("(%d+):(%d+):(%d+)(%.%d+)")
160       local nv = hours*3600+minutes*60+seconds+frac
161       m:Op(k..":="..v.." -> "..nv)
162       --m:Op(k.."@sec="..nv)
163       return "multi"
164    end
165 )
166
167 DOC.engines = DOC.engines.."\n"..[[
168  date_split
169  date_split_de
170   Split dates into subcomponents, one for international notation ("year/month/day hour:minute:second" or
171   "year:month:day hour:minute:second") and one for german notaton ("day.month.year hour:minute:second").
172 ]]
173 Meta:Engine (
174    "date_split",
175    function (m, f, _, k, v)
176       local year,month,day,hour,minute,second = v:match("(%d*)[:/](%d*)[:/](%d*)%s*(%d*):(%d*):(%d*)")
177       m:Op(k..".year="..year)
178       m:Op(k..".month="..month)
179       m:Op(k..".day="..day)
180       m:Op(k..".hour="..hour)
181       m:Op(k..".minute="..minute)
182       m:Op(k..".second="..second)
183       return "multi"
184    end
185 )
186 Meta:Engine (
187    "date_split_de",
188    function (m, f, k, v)
189       local day,month,year,hour,minute,second = v:match("(%d*)[:/](%d*)[:/](%d*)%s*(%d*):(%d*):(%d*)")
190       m:Op(k..".year="..year)
191       m:Op(k..".month="..month)
192       m:Op(k..".day="..day)
193       m:Op(k..".hour="..hour)
194       m:Op(k..".minute="..minute)
195       m:Op(k..".second="..second)
196       return "multi"
197    end
198 )
199
200 local date_events = {}
201 function DateEvent(from, to, tagger)
202    table.insert (date_events, from)
203    table.insert (date_events, to)
204    if type(tagger) == 'function' then
205       table.insert (date_events, tagger)
206    else
207       table.insert (date_events,
208                     function (m)
209                        m:Op(tagger)
210                     end
211                  )
212    end
213 end
214
215
216 DOC.engines = DOC.engines.."\n"..[[
217  event_by_date
218   Adds metadata according to date matches. The actual metadata to be added has to be configured with
219   the DateEvent(from, to, tag) function first. Dates must match the "year[:/]?month[:/]?day%s*hour:?minute"
220   pattern. For each date a begin and a end date can be specified. The month, day, hour and minute fields are
221   subsequently optional. The tag can be a metadata instruction in the "field[+-?:]=value" format or some
222   custom function with the same prototype as any engine to manipulate existng metadata.
223
224   There is a default rule for `exif.image_created=` metadata.
225
226   Example for adding dates:
227    DateEvent("2006/6/1", "2006/6/14", "vacation=summer2006")
228
229   Will tag all images created in that timespan with the respective vacation metadata.
230 ]]
231 Meta:Engine (
232    "event_by_date",
233    function (m, f, _, k, v)
234       for i=1,#date_events,3 do
235          local actual = table.pack(      v:match("(%d*)[:/]?(%d*)[:/]?(%d*)%s*(%d*):?(%d*)"))
236          local from = table.pack(date_events[i]:match("(%d*)[:/]?(%d*)[:/]?(%d*)%s*(%d*):?(%d*)"))
237          local to = table.pack(date_events[i+1]:match("(%d*)[:/]?(%d*)[:/]?(%d*)%s*(%d*):?(%d*)"))
238          local ok = true
239
240          for i=1,#actual do
241             if #from[i] > 0 then
242                if not (actual[i] >= from[i]) then
243                   ok=false
244                   break
245                end
246             end
247             if #to[i] > 0 then
248                if not (actual[i] <= to[i]) then
249                   ok=false
250                   break
251                end
252             end
253          end
254
255          if ok then
256             date_events[i+2] (m, f, k, v)
257          end
258
259       end
260       return "multi"
261    end
262 )
263
264
265 DOC.engines = DOC.engines.."\n"..[[
266  exiftags
267   Calls the exiftags utility and adds all metadata it reports with a 'exif.' prefix to the file.
268
269   By default invoked on all jpeg images.
270 ]]
271 if shexists("exiftags") then
272    Meta:Engine (
273       "exiftags",
274       function (m, f)
275          local tags = io.popen("exiftags -q "..shquote(f).." 2>/dev/null")
276          for line in tags:lines() do
277             local tag, value = string.match(line, "(.-): (.*)")
278             if tag and value then
279                tag = "exif."..string.gsub(tag, "[ ]", "_")
280                tag = tag:lower()
281                dmsg(tag, value)
282                m:Op(tag.."+="..value)
283             end
284          end
285          io.close(tags)
286       end
287    )
288 else
289    msg("exiftags not found")
290 end
291
292 DOC.engines = DOC.engines.."\n"..[[
293  mplayer
294   Calls `mplayer` with options to query all kinds of metadata from a file.
295   Postprocesses this gathered metadata a bit (see source) and adds it to the file.
296 ]]
297 if shexists("mplayer") then
298
299    -- certain mplayer generated tags needs some special masssaging, this can be either a function or just 'false' to drop the tag
300    local mplayer_filter = {
301       {
302          "^clip%.info_name%d*",
303          function (m, meta, key, value)
304             local n = string.match(key,"^clip%.info_name(%d*)")
305             local values2 = meta["clip.info_value"..n]
306             for v2=1,#values2 do
307                m:Op(value.."+="..values2[v2])
308             end
309          end
310       },
311       {
312          -- already handled above
313          "^clip%.info_value%d*", false
314       },
315       {
316          "^filename", false
317       },
318       {
319          "^exit", false
320       },
321       {
322          "^chapter%.id", false
323       },
324       {
325          "^video%.id", false
326       },
327       {
328          "^audio%.id", false
329       },
330       {
331          "^subtitle%.id", false
332       }
333    }
334
335
336    Meta:Engine (
337       "mplayer",
338       function (m, f)
339          local tags = io.popen("mplayer -noconfig all -cache-min 0 -vo null -ao null -frames 0 -identify "..shquote(f).." 2>/dev/null")
340          local meta = {}
341
342          for line in tags:lines() do
343             local tag, value = string.match(line, "^ID_(.-)=(.*)")
344             if tag and value then
345                tag = string.gsub(tag, "[_]", ".", 1)
346                tag = string.gsub(tag, "[ ]", "_")
347                tag = tag:lower()
348                if not meta[tag] then
349                   meta[tag] = {value}
350                else
351                   table.insert(meta[tag], value)
352                end
353             end
354          end
355          io.close(tags)
356
357          for tag,values in pairs(meta) do
358             for v=1,#values do
359                local value = values[v]
360                for f=1,#mplayer_filter do
361                   if string.match(tag,mplayer_filter[f][1]) then
362                      if type(mplayer_filter[f][2]) == 'function' then
363                         mplayer_filter[f][2](m, meta, tag, value)
364                         value = nil
365                         break
366                      elseif not mplayer_filter[f][2] then
367                         value = nil
368                         break
369                      end
370                   end
371                end
372
373                if tag and value then
374                   m:Op(tag.."+="..values[v])
375                end
376             end
377          end
378
379       end
380    )
381 else
382    msg("mplayer not found")
383 end
384