123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- --[[
- -- Copyright (c) 2013-2016 Marcus Rohrmoser, http://purl.mro.name/recorder
- --
- -- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
- -- associated documentation files (the "Software"), to deal in the Software without restriction,
- -- including without limitation the rights to use, copy, modify, merge, publish, distribute,
- -- sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
- -- furnished to do so, subject to the following conditions:
- --
- -- The above copyright notice and this permission notice shall be included in all copies or
- -- substantial portions of the Software.
- --
- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
- -- NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- -- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
- -- OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- -- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- --
- -- MIT License http://opensource.org/licenses/MIT
- ]]
- local function meta_key_to_lua(k)
- return k:gsub('%.', '_')
- end
- function string:to_filename()
- local replace = {
- ['/'] = '-',
- ["\t"] = ' ',
- ["\n"] = ' ',
- }
- local escape = {
- }
- local subf = function(s)
- return replace[s] or escape[s] or s
- end
- return self:gsub('.', subf):gsub('–', '-')
- end
- -------------------------------------------------------------------------------
- -- Broadcast ------------------------------------------------------------------
- -------------------------------------------------------------------------------
- require'Station'
- require'Podcast'
- require'Enclosure'
- -- http://nova-fusion.com/2011/06/30/lua-metatables-tutorial/
- -- http://lua-users.org/wiki/LuaClassesWithMetatable
- Broadcast = {} -- methods table
- Broadcast_mt = { __index = Broadcast } -- metatable
- function Broadcast_mt.__eq(a,b)
- return a.id == b.id
- end
- -- compare acc. time, ignore dst switch.
- function Broadcast_mt.__le(a,b)
- if a.year < b.year then return true
- elseif a.year > b.year then return false
- elseif a.month < b.month then return true
- elseif a.month > b.month then return false
- elseif a.day < b.day then return true
- elseif a.day > b.day then return false
- elseif a.hour < b.hour then return true
- elseif a.hour > b.hour then return false
- elseif a.min < b.min then return true
- elseif a.min > b.min then return false
- elseif a.sec < b.sec then return true
- elseif a.sec > b.sec then return false
- else return a.id <= b.id end
- end
- function Broadcast_mt.__lt(a,b)
- return a <= b and not (b <= a)
- end
- function Broadcast_mt.__tostring(self)
- return self.id
- end
- local function factory(ret)
- local file = assert(ret._title):to_filename()
- local t = os.time(ret)
- ret.dir = table.concat{assert(ret._station).id, '/', os.date('%Y/%m/%d', t)}
- ret.id = table.concat{ret.dir, '/', os.date('%H%M', t), ' ', file}
- return setmetatable( ret, Broadcast_mt )
- end
- function Broadcast.from_meta(meta)
- local pbmi = {
- DC_scheme = assert( meta.DC_scheme ),
- DC_language = assert( meta.DC_language ),
- DC_title = assert( meta.DC_title ),
- DC_title_series = meta.DC_title_series,
- DC_title_episode = meta.DC_title_episode,
- DC_subject = meta.DC_subject,
- DC_format_timestart = assert( meta.DC_format_timestart ),
- DC_format_timeend = assert( meta.DC_format_timeend ),
- DC_format_duration = assert( meta.DC_format_duration ),
- DC_image = meta.DC_image,
- DC_description = assert( meta.DC_description ),
- DC_author = meta.DC_author,
- DC_creator = meta.DC_creator,
- DC_publisher = meta.DC_publisher,
- DC_copyright = meta.DC_copyright,
- DC_source = assert( meta.DC_source ),
- }
- local ret = os.date('*t', assert(parse_iso8601(meta.DC_format_timestart, 'missing key \'DC_format_timestart\'')))
- ret._station = assert(Station.from_id(meta.station), 'missing key \'station\'')
- ret._dtend = assert(parse_iso8601(meta.DC_format_timeend, 'missing key \'DC_format_timeend\''))
- ret._title = assert(meta.title)
- ret._pbmi = pbmi
- return factory(ret)
- end
- function Broadcast.from_id(f)
- local ok,_,station,year,month,day,hour,min,title = f:find('([^/]+)/(%d%d%d%d)/(%d%d)/(%d%d)/(%d%d)(%d%d)%s(.+)$')
- if not ok then return nil end
- local ret = os.date('*t', os.time{year=year,month=month,day=day,hour=hour,min=min})
- ret._station = assert(Station.from_id(station), 'missing key \'station\': ' .. f)
- ret._title = assert(title, 'missing title')
- return factory(ret)
- end
- function Broadcast.from_file(f)
- -- strip prefix, keep id
- local ok,_,id,ext = f:find('([^/]+/%d%d%d%d/%d%d/%d%d/%d%d%d%d .+)$')
- if not ok then return nil end
- -- strip extension
- local ok,_,t,ext = id:find('(.+)(%.[^%.]+)$')
- if ok and ('.xml' == ext or '.json' == ext) then id = t end
- return Broadcast.from_id(id)
- end
- function Broadcast:modified()
- return lfs.attributes(self:filename('xml'), 'modification')
- end
- function Broadcast:title()
- return assert(self._title)
- end
- function Broadcast:station()
- return assert(self._station)
- end
- function Broadcast:dtstart()
- if not self._dtstart then
- self._dtstart = assert(parse_iso8601(self:pbmi().DC_format_timestart))
- end
- return self._dtstart
- end
- function Broadcast:dtend()
- if not self._dtend then
- self._dtend = assert(parse_iso8601(self:pbmi().DC_format_timeend))
- end
- return self._dtend
- end
- function Broadcast:is_past(now)
- if now == nil then now = os.time() end
- if os.time(self) < now then
- -- already started, but still running?
- if self:dtend() < now then
- -- io.stderr:write('I\'m past: ', self.id, "\n")
- return true
- end
- end
- return false
- end
- -- return table w. <meta> plus xml source
- -- TODO: sanity check found meta!
- local function broadcast_meta_from_xml(xml_file)
- local metas,xml_old,file = {},nil,io.open(xml_file, 'r')
- if file == nil then return nil,nil,xml_file end
- local xml_old = file:read('*a')
- file:close()
- for v,k in xml_old:gmatch('<meta%s+content=\'([^\']*)\'%s+name=\'(DC%.[^\']+)\'%s*/?>') do
- local k,v = k:unescape_xml_text(),v:unescape_xml_text()
- metas[ meta_key_to_lua(k) ] = v
- end
- return metas,xml_old,xml_file
- end
- function Broadcast:filename(state)
- local t = {'stations', '/', self.id}
- if state then
- table.insert(t, '.')
- table.insert(t, state)
- end
- return table.concat(t)
- end
- function Broadcast:url(type_)
- return table.concat{Recorder.base_url(), self:filename(type_)}:escape_url()
- end
- -- accessor to Dublin Core PBMI http://dcpapers.dublincore.org/pubs/article/view/749
- function Broadcast:pbmi()
- if not self._pbmi then
- self._pbmi,_,file = broadcast_meta_from_xml(self:filename('xml'))
- assert(self._pbmi,'Couldn\'t load pbmi from ' .. file)
- end
- return assert(self._pbmi, 'lazy load failure.')
- end
- function Broadcast:enclosure()
- if not self._enclosure then
- self._enclosure = Enclosure.from_broadcast(self)
- end
- return self._enclosure
- end
- function Broadcast:add_podcast(pc)
- self:podcasts()[pc.id] = pc
- end
- function Broadcast:remove_podcast(pc)
- self:podcasts()[pc.id] = nil
- pc:remove_broadcast(self)
- end
- function Broadcast:podcasts()
- if not self._podcasts then
- local ret = {}
- for pi_id,pc in pairs(Podcast.each()) do
- if pc:contains_broadcast(self) then
- ret[pi_id] = pc
- end
- -- check presence ?
- -- evtl. check match ?
- -- add to podcast ?
- -- add to list ?
- end
- self._podcasts = ret
- end
- return self._podcasts
- end
- function Broadcast:match_podcasts()
- for pc_id,pc in pairs( Podcast.each() ) do
- local ok,match = pcall(assert(pc.match, 'match'), assert(self:pbmi(), 'pbmi'))
- if ok and match then
- self:podcasts()[pc_id] = pc
- end
- end
- end
- -- find first one smaller or equal dtstart
- function Broadcast:prev_sibling()
- return self:station():broadcast_now(self:dtstart()-1,true,false)
- end
- -- find first one bigger or equal dtend
- function Broadcast:next_sibling()
- return self:station():broadcast_now(self:dtend(),true,true)
- end
- function Broadcast:monopolize(dry_run)
- local callb = function( path )
- local other = Broadcast.from_file( path )
- if other ~= nil and other ~= self then other:remove(dry_run) end
- return false
- end
- lfs.files_between(table.concat({'stations',self:station().id},'/'), self:dtstart(), self:dtend()-0.1, callb, true)
- end
- function Broadcast:log_change(msg)
- io.stderr:write(string.format("%-7s %s\n",msg,self.id))
- if 'unchang' == msg then return end
- local f,_ = io.open(table.concat({'stations','modified.ttl'},'/'), 'a+')
- if f then
- f:write('<', self.id:escape_url(), '> <http://purl.org/dc/terms/modified> "', os.date('!%FT%TZ'), "\" .\n")
- f:close()
- end
- f,_ = io.open(table.concat({'stations',self:station().id,'modified.ttl'},'/'), 'a+')
- if f then
- f:write('<../', self.id:escape_url(), '> <http://purl.org/dc/terms/modified> "', os.date('!%FT%TZ'), "\" .\n")
- f:close()
- end
- end
- function Broadcast:remove(dry_run)
- self:log_change('delete')
- if dry_run then return end
- self:enclosure():unschedule()
- for _,pc in pairs(self:podcasts()) do
- pc:remove_broadcast(self)
- end
- io.write_if_changed(self:filename('json'), nil)
- io.write_if_changed(self:filename('xml'), nil)
- end
- function Broadcast:to_xml(xml)
- -- TODO check time overlaps?
- if not xml then xml = {} end
- table.insert( xml, '<!-- unorthodox relative namespace to enable http://www.w3.org/TR/grddl-tests/#sq2 without a central server -->' )
- table.insert( xml, '<broadcast xml:lang="de" xmlns="../../../../../assets/2013/radio-pi.rdf">' )
- local row = {' ', '<meta content=\'', self.id:escape_xml_attribute(), '\' name=\'', 'DC.identifier', '\'/>'}
- table.insert( xml, table.concat(row) )
- for _,k in ipairs({
- 'DC.scheme', 'DC.language', 'DC.title', 'DC.title.series', 'DC.title.episode', 'DC.subject',
- 'DC.format.timestart', 'DC.format.timeend', 'DC.format.duration', 'DC.image',
- 'DC.description', 'DC.author', 'DC.publisher', 'DC.creator', 'DC.copyright', 'DC.source',
- }) do
- local v = self:pbmi()[ meta_key_to_lua(k) ]
- if v then
- local row = {' ', '<meta content=\'', v:escape_xml_attribute(), '\' name=\'', k, '\'/>'}
- table.insert( xml, table.concat(row) )
- end
- end
- table.insert( xml, '</broadcast>' )
- return table.concat(xml,"\n")
- end
- function Broadcast:save_xml()
- -- TODO check time overlaps?
- local xml = {
- '<?xml version="1.0" encoding="UTF-8"?>',
- '<?xml-stylesheet type="text/xsl" href="../../../app/broadcast2html.xslt"?>',
- }
- self:to_xml(xml)
- return io.write_if_changed(self:filename('xml'), table.concat(xml,"\n"))
- end
- function Broadcast:save_podcast_json()
- -- io.stderr:write('to_podcast_json()', "\n")
- local pc_ids = {}
- for pc_id,pc in pairs( self:podcasts() ) do
- pc:add_broadcast(self)
- table.insert(pc_ids, pc.id)
- end
- local json = nil
- if #pc_ids > 0 then
- json = table.concat{'{ "podcasts":[{"name":"', table.concat(pc_ids,'"},{"name":"'), '"}] }'}
- end
- return io.write_if_changed(self:filename('json'), json)
- end
- function Broadcast:save_schedule()
- for _,_ in pairs(self:podcasts()) do
- -- no way to tell count of a hash - so we start to iterate and return after first
- return self:enclosure():schedule()
- end
- return self:enclosure():unschedule()
- end
- function Broadcast:save()
- self:monopolize()
- -- broadcast xml
- local file,msg,err = self:save_xml()
- self:log_change(msg)
- -- podcast membership
- for _,pc in pairs(self:podcasts()) do
- pc:add_broadcast(self)
- end
- self:save_podcast_json()
- -- schedule
- local at_job,cmd = self:save_schedule()
- -- if at_job then io.stderr:write('at job ', at_job, ' ', cmd, "\n") end
- return file,msg,err
- end
|