| 1 | -- Copyright (C) 2011-2015 Anton Burdinuk
|
|---|
| 2 | -- clark15b@gmail.com
|
|---|
| 3 | -- https://tsdemuxer.googlecode.com/svn/trunk/xupnpd
|
|---|
| 4 |
|
|---|
| 5 | http.sendurl_buffer_size(32768,1);
|
|---|
| 6 |
|
|---|
| 7 | if cfg.daemon==true then core.detach() end
|
|---|
| 8 |
|
|---|
| 9 | core.openlog(cfg.log_ident,cfg.log_facility)
|
|---|
| 10 |
|
|---|
| 11 | if cfg.daemon==true then core.touchpid(cfg.pid_file) end
|
|---|
| 12 |
|
|---|
| 13 | if cfg.embedded==true then cfg.debug=0 end
|
|---|
| 14 |
|
|---|
| 15 | function clone_table(t)
|
|---|
| 16 | local tt={}
|
|---|
| 17 | for i,j in pairs(t) do
|
|---|
| 18 | tt[i]=j
|
|---|
| 19 | end
|
|---|
| 20 | return tt
|
|---|
| 21 | end
|
|---|
| 22 |
|
|---|
| 23 | function split_string(s,d)
|
|---|
| 24 | local t={}
|
|---|
| 25 | d='([^'..d..']+)'
|
|---|
| 26 | for i in string.gmatch(s,d) do
|
|---|
| 27 | table.insert(t,i)
|
|---|
| 28 | end
|
|---|
| 29 | return t
|
|---|
| 30 | end
|
|---|
| 31 |
|
|---|
| 32 | function load_plugins(path,what)
|
|---|
| 33 | local d=util.dir(path)
|
|---|
| 34 |
|
|---|
| 35 | if d then
|
|---|
| 36 | for i,j in ipairs(d) do
|
|---|
| 37 | if string.find(j,'^[%w_-]+%.lua$') then
|
|---|
| 38 | if cfg.debug>0 then print(what..' \''..j..'\'') end
|
|---|
| 39 | dofile(path..j)
|
|---|
| 40 | end
|
|---|
| 41 | end
|
|---|
| 42 | end
|
|---|
| 43 | end
|
|---|
| 44 |
|
|---|
| 45 |
|
|---|
| 46 | -- options for profiles
|
|---|
| 47 | cfg.dev_desc_xml='/dev.xml' -- UPnP Device Description XML
|
|---|
| 48 | cfg.upnp_container='object.container' -- UPnP class for containers
|
|---|
| 49 | cfg.upnp_artist=false -- send <upnp:artist> / <upnp:actor> in SOAP response
|
|---|
| 50 | cfg.upnp_feature_list='' -- X_GetFeatureList response body
|
|---|
| 51 | cfg.upnp_albumart=0 -- 0: <upnp:albumArtURI>direct url</upnp:albumArtURI>, 1: <res>direct url<res>, 2: <upnp:albumArtURI>local url</upnp:albumArtURI>, 3: <res>local url<res>
|
|---|
| 52 | cfg.dlna_headers=true -- send TransferMode.DLNA.ORG and ContentFeatures.DLNA.ORG in HTTP response
|
|---|
| 53 | cfg.dlna_extras=true -- DLNA extras in headers and SOAP
|
|---|
| 54 | cfg.content_disp=false -- send Content-Disposition when streaming
|
|---|
| 55 | cfg.soap_length=true -- send Content-Length in SOAP response
|
|---|
| 56 | cfg.wdtv=false -- WDTV Live compatible mode
|
|---|
| 57 | cfg.sec_extras=false -- Samsung extras
|
|---|
| 58 |
|
|---|
| 59 |
|
|---|
| 60 | update_id=1 -- system update_id
|
|---|
| 61 |
|
|---|
| 62 | subscr={} -- event sessions (for UPnP notify engine)
|
|---|
| 63 | plugins={} -- external plugins (YouTube, Vimeo ...)
|
|---|
| 64 | profiles={} -- device profiles
|
|---|
| 65 | cache={} -- real URL cache for plugins
|
|---|
| 66 | cache_size=0
|
|---|
| 67 |
|
|---|
| 68 | if not cfg.feeds_path then cfg.feeds_path=cfg.playlists_path end
|
|---|
| 69 |
|
|---|
| 70 | -- create feeds directory
|
|---|
| 71 | if cfg.feeds_path~=cfg.playlists_path then os.execute('mkdir -p '..cfg.feeds_path) end
|
|---|
| 72 |
|
|---|
| 73 | -- load config, plugins and profiles
|
|---|
| 74 | load_plugins(cfg.plugin_path,'plugin')
|
|---|
| 75 | load_plugins(cfg.config_path,'config')
|
|---|
| 76 |
|
|---|
| 77 | dofile('xupnpd_mime.lua')
|
|---|
| 78 |
|
|---|
| 79 | if cfg.profiles then load_plugins(cfg.profiles,'profile') end
|
|---|
| 80 |
|
|---|
| 81 | dofile('xupnpd_m3u.lua')
|
|---|
| 82 | dofile('xupnpd_ssdp.lua')
|
|---|
| 83 | dofile('xupnpd_http.lua')
|
|---|
| 84 |
|
|---|
| 85 | -- download feeds from external sources (child process)
|
|---|
| 86 | function update_feeds_async()
|
|---|
| 87 | local num=0
|
|---|
| 88 | for i,j in ipairs(feeds) do
|
|---|
| 89 | local plugin=plugins[ j[1] ]
|
|---|
| 90 | if plugin and plugin.disabled~=true and plugin.updatefeed then
|
|---|
| 91 | if plugin.updatefeed(j[2],j[3])==true then num=num+1 end
|
|---|
| 92 | end
|
|---|
| 93 | end
|
|---|
| 94 |
|
|---|
| 95 | if num>0 then core.sendevent('reload') end
|
|---|
| 96 |
|
|---|
| 97 | end
|
|---|
| 98 |
|
|---|
| 99 | -- spawn child process for feeds downloading
|
|---|
| 100 | function update_feeds(what,sec)
|
|---|
| 101 | core.fspawn(update_feeds_async)
|
|---|
| 102 | core.timer(cfg.feeds_update_interval,what)
|
|---|
| 103 | end
|
|---|
| 104 |
|
|---|
| 105 |
|
|---|
| 106 | -- subscribe player for ContentDirectory events
|
|---|
| 107 | function subscribe(event,sid,callback,ttl)
|
|---|
| 108 | local s=nil
|
|---|
| 109 |
|
|---|
| 110 | if subscr[sid] then
|
|---|
| 111 | s=subscr[sid]
|
|---|
| 112 | s.timestamp=os.time()
|
|---|
| 113 | else
|
|---|
| 114 | if callback=='' then return end
|
|---|
| 115 | s={}
|
|---|
| 116 | subscr[sid]=s
|
|---|
| 117 | s.event=event
|
|---|
| 118 | s.sid=sid
|
|---|
| 119 | s.callback=callback
|
|---|
| 120 | s.timestamp=os.time()
|
|---|
| 121 | s.ttl=tonumber(ttl)
|
|---|
| 122 | s.seq=0
|
|---|
| 123 | end
|
|---|
| 124 |
|
|---|
| 125 | if cfg.debug>0 then print('subscribe: '..s.sid..', '..s.event..', '..s.callback) end
|
|---|
| 126 |
|
|---|
| 127 | end
|
|---|
| 128 |
|
|---|
| 129 | -- unsubscribe player
|
|---|
| 130 | function unsubscribe(sid)
|
|---|
| 131 | if subscr[sid] then
|
|---|
| 132 | subscr[sid]=nil
|
|---|
| 133 |
|
|---|
| 134 | if cfg.debug>0 then print('unsubscribe: '..sid) end
|
|---|
| 135 | end
|
|---|
| 136 | end
|
|---|
| 137 |
|
|---|
| 138 | --store to cache
|
|---|
| 139 | function cache_store(k,v)
|
|---|
| 140 | local time=os.time()
|
|---|
| 141 |
|
|---|
| 142 | local cc=cache[k]
|
|---|
| 143 |
|
|---|
| 144 | if cc then cc.value=v cc.time=time return end
|
|---|
| 145 |
|
|---|
| 146 | if cache_size>=cfg.cache_size then
|
|---|
| 147 | local min_k=nil
|
|---|
| 148 | local min_time=nil
|
|---|
| 149 | for i,j in pairs(cache) do
|
|---|
| 150 | if not min_time or min_time>j.time then min_k=i min_time=j.time end
|
|---|
| 151 | end
|
|---|
| 152 | if min_k then
|
|---|
| 153 | if cfg.debug>0 then print('remove URL from cache (overflow): '..min_k) end
|
|---|
| 154 | cache[min_k]=nil
|
|---|
| 155 | cache_size=cache_size-1
|
|---|
| 156 | end
|
|---|
| 157 | end
|
|---|
| 158 |
|
|---|
| 159 | local t={}
|
|---|
| 160 | t.time=time
|
|---|
| 161 | t.value=v
|
|---|
| 162 | cache[k]=t
|
|---|
| 163 | cache_size=cache_size+1
|
|---|
| 164 | end
|
|---|
| 165 |
|
|---|
| 166 |
|
|---|
| 167 | -- garbage collection
|
|---|
| 168 | function sys_gc(what,sec)
|
|---|
| 169 |
|
|---|
| 170 | local t=os.time()
|
|---|
| 171 |
|
|---|
| 172 | -- force unsubscribe
|
|---|
| 173 | local g={}
|
|---|
| 174 |
|
|---|
| 175 | for i,j in pairs(subscr) do
|
|---|
| 176 | if os.difftime(t,j.timestamp)>=j.ttl then
|
|---|
| 177 | table.insert(g,i)
|
|---|
| 178 | end
|
|---|
| 179 | end
|
|---|
| 180 |
|
|---|
| 181 | for i,j in ipairs(g) do
|
|---|
| 182 | subscr[j]=nil
|
|---|
| 183 |
|
|---|
| 184 | if cfg.debug>0 then print('force unsubscribe (timeout): '..j) end
|
|---|
| 185 | end
|
|---|
| 186 |
|
|---|
| 187 | -- cache clear
|
|---|
| 188 | g={}
|
|---|
| 189 |
|
|---|
| 190 | for i,j in pairs(cache) do
|
|---|
| 191 | if os.difftime(t,j.time)>=cfg.cache_ttl then
|
|---|
| 192 | table.insert(g,i)
|
|---|
| 193 | end
|
|---|
| 194 | end
|
|---|
| 195 |
|
|---|
| 196 | cache_size=cache_size-table.maxn(g)
|
|---|
| 197 |
|
|---|
| 198 | for i,j in ipairs(g) do
|
|---|
| 199 | cache[j]=nil
|
|---|
| 200 |
|
|---|
| 201 | if cfg.debug>0 then print('remove URL from cache (timeout): '..j) end
|
|---|
| 202 | end
|
|---|
| 203 |
|
|---|
| 204 | core.timer(sec,what)
|
|---|
| 205 | end
|
|---|
| 206 |
|
|---|
| 207 |
|
|---|
| 208 | -- ContentDirectory event deliver (child process)
|
|---|
| 209 | function subscr_notify_iterate_tree(pls,tt)
|
|---|
| 210 | if pls.elements then
|
|---|
| 211 | table.insert(tt,pls.objid..','..update_id)
|
|---|
| 212 |
|
|---|
| 213 | for i,j in ipairs(pls.elements) do
|
|---|
| 214 | subscr_notify_iterate_tree(j,tt)
|
|---|
| 215 | end
|
|---|
| 216 | end
|
|---|
| 217 | end
|
|---|
| 218 |
|
|---|
| 219 | function subscr_notify_async(t)
|
|---|
| 220 |
|
|---|
| 221 | local tt={}
|
|---|
| 222 | subscr_notify_iterate_tree(playlist_data,tt)
|
|---|
| 223 |
|
|---|
| 224 | local data=string.format(
|
|---|
| 225 | '<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\"><e:property><SystemUpdateID>%s</SystemUpdateID><ContainerUpdateIDs>%s</ContainerUpdateIDs></e:property></e:propertyset>',
|
|---|
| 226 | update_id,table.concat(tt,','))
|
|---|
| 227 |
|
|---|
| 228 | for i,j in ipairs(t) do
|
|---|
| 229 | if cfg.debug>0 then print('notify: '..j.callback..', sid='..j.sid..', seq='..j.seq) end
|
|---|
| 230 | http.notify(j.callback,j.sid,data,j.seq)
|
|---|
| 231 | end
|
|---|
| 232 | end
|
|---|
| 233 |
|
|---|
| 234 |
|
|---|
| 235 | -- reload all playlists
|
|---|
| 236 | function reload_playlist()
|
|---|
| 237 | reload_playlists()
|
|---|
| 238 | update_id=update_id+1
|
|---|
| 239 |
|
|---|
| 240 | if update_id>100000 then update_id=1 end
|
|---|
| 241 |
|
|---|
| 242 | if cfg.debug>0 then print('reload playlist, update_id='..update_id) end
|
|---|
| 243 |
|
|---|
| 244 | if cfg.dlna_notify==true then
|
|---|
| 245 | local t={}
|
|---|
| 246 |
|
|---|
| 247 | for i,j in pairs(subscr) do
|
|---|
| 248 | if j.event=='cds' then
|
|---|
| 249 | table.insert(t, { ['callback']=j.callback, ['sid']=j.sid, ['seq']=j.seq } )
|
|---|
| 250 | j.seq=j.seq+1
|
|---|
| 251 | if j.seq>100000 then j.seq=0 end
|
|---|
| 252 | end
|
|---|
| 253 | end
|
|---|
| 254 |
|
|---|
| 255 | if table.maxn(t)>0 then
|
|---|
| 256 | core.fspawn(subscr_notify_async,t)
|
|---|
| 257 | end
|
|---|
| 258 | end
|
|---|
| 259 | end
|
|---|
| 260 |
|
|---|
| 261 | -- change child process status (for UI)
|
|---|
| 262 | function set_child_status(pid,status)
|
|---|
| 263 | pid=tonumber(pid)
|
|---|
| 264 | if childs[pid] then
|
|---|
| 265 | childs[pid].status=status
|
|---|
| 266 | childs[pid].time=os.time()
|
|---|
| 267 | end
|
|---|
| 268 | end
|
|---|
| 269 |
|
|---|
| 270 | function get_drive_state(drive)
|
|---|
| 271 | local s
|
|---|
| 272 |
|
|---|
| 273 | local f=io.popen('/sbin/hdparm -C '..drive..' 2>/dev/null | grep -i state','r')
|
|---|
| 274 |
|
|---|
| 275 | if f then
|
|---|
| 276 | s=f:read('*a')
|
|---|
| 277 | f:close()
|
|---|
| 278 | end
|
|---|
| 279 |
|
|---|
| 280 | return string.match(s,'drive state is:%s+(.+)%s+')
|
|---|
| 281 | end
|
|---|
| 282 |
|
|---|
| 283 |
|
|---|
| 284 | function profile_change(user_agent,req)
|
|---|
| 285 | if not user_agent or user_agent=='' then return end
|
|---|
| 286 |
|
|---|
| 287 | for name,profile in pairs(profiles) do
|
|---|
| 288 | local match=profile.match
|
|---|
| 289 |
|
|---|
| 290 | if profile.disabled~=true and match and match(user_agent,req) then
|
|---|
| 291 |
|
|---|
| 292 | local options=profile.options
|
|---|
| 293 | local mtypes=profile.mime_types
|
|---|
| 294 |
|
|---|
| 295 | if options then for i,j in pairs(options) do cfg[i]=j end end
|
|---|
| 296 |
|
|---|
| 297 | if mtypes then
|
|---|
| 298 | if profile.replace_mime_types==true then
|
|---|
| 299 | mime=mtypes
|
|---|
| 300 | else
|
|---|
| 301 | for i,j in pairs(mtypes) do mime[i]=j end
|
|---|
| 302 | end
|
|---|
| 303 | end
|
|---|
| 304 |
|
|---|
| 305 | return name
|
|---|
| 306 | end
|
|---|
| 307 | end
|
|---|
| 308 | return nil
|
|---|
| 309 | end
|
|---|
| 310 |
|
|---|
| 311 |
|
|---|
| 312 | -- event handlers
|
|---|
| 313 | events['SIGUSR1']=reload_playlist
|
|---|
| 314 | events['reload']=reload_playlist
|
|---|
| 315 | events['store']=cache_store
|
|---|
| 316 | events['sys_gc']=sys_gc
|
|---|
| 317 | events['subscribe']=subscribe
|
|---|
| 318 | events['unsubscribe']=unsubscribe
|
|---|
| 319 | events['update_feeds']=update_feeds
|
|---|
| 320 | events['status']=set_child_status
|
|---|
| 321 | events['config']=function() load_plugins(cfg.config_path,'config') cache={} cache_size=0 end
|
|---|
| 322 | events['remove_feed']=function(id) table.remove(feeds,tonumber(id)) end
|
|---|
| 323 | events['add_feed']=function(plugin,feed,name) table.insert(feeds,{[1]=plugin,[2]=feed,[3]=name}) end
|
|---|
| 324 | events['plugin']=function(name,status) if status=='on' then plugins[name].disabled=false else plugins[name].disabled=true end end
|
|---|
| 325 | events['profile']=function(name,status) if status=='on' then profiles[name].disabled=false else profiles[name].disabled=true end end
|
|---|
| 326 | events['bookmark']=function(objid,pos) local pls=find_playlist_object(objid) if pls then pls.bookmark=pos end end
|
|---|
| 327 |
|
|---|
| 328 | events['update_playlists']=
|
|---|
| 329 | function(what,sec)
|
|---|
| 330 | if cfg.drive and cfg.drive~='' then
|
|---|
| 331 | if get_drive_state(cfg.drive)=='active/idle' then
|
|---|
| 332 | reload_playlist()
|
|---|
| 333 | end
|
|---|
| 334 | else
|
|---|
| 335 | reload_playlist()
|
|---|
| 336 | end
|
|---|
| 337 |
|
|---|
| 338 | core.timer(cfg.playlists_update_interval,what)
|
|---|
| 339 | end
|
|---|
| 340 |
|
|---|
| 341 |
|
|---|
| 342 | if cfg.embedded==true then print=function () end end
|
|---|
| 343 |
|
|---|
| 344 | -- start garbage collection system
|
|---|
| 345 | core.timer(300,'sys_gc')
|
|---|
| 346 |
|
|---|
| 347 | http.timeout(cfg.http_timeout)
|
|---|
| 348 | http.user_agent(cfg.user_agent)
|
|---|
| 349 |
|
|---|
| 350 | -- start feeds update system
|
|---|
| 351 | if cfg.feeds_update_interval>0 then
|
|---|
| 352 | core.timer(3,'update_feeds')
|
|---|
| 353 | end
|
|---|
| 354 |
|
|---|
| 355 | if cfg.playlists_update_interval>0 then
|
|---|
| 356 | core.timer(cfg.playlists_update_interval,'update_playlists')
|
|---|
| 357 | end
|
|---|
| 358 |
|
|---|
| 359 | load_plugins(cfg.config_path..'postinit/','postinit')
|
|---|
| 360 |
|
|---|
| 361 | print("start "..cfg.log_ident)
|
|---|
| 362 |
|
|---|
| 363 | core.mainloop()
|
|---|
| 364 |
|
|---|
| 365 | print("stop "..cfg.log_ident)
|
|---|
| 366 |
|
|---|
| 367 | if cfg.daemon==true then os.execute('rm -f '..cfg.pid_file) end
|
|---|