1 | -- Copyright (C) 2011-2013 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={} |
---|
109 | subscr[sid]=s |
---|
110 | |
---|
111 | s.event=event |
---|
112 | s.sid=sid |
---|
113 | s.callback=callback |
---|
114 | s.timestamp=os.time() |
---|
115 | s.ttl=tonumber(ttl) |
---|
116 | s.seq=0 |
---|
117 | |
---|
118 | if cfg.debug>0 then print('subscribe: '..sid..', '..event..', '..callback) end |
---|
119 | |
---|
120 | end |
---|
121 | |
---|
122 | -- unsubscribe player |
---|
123 | function unsubscribe(sid) |
---|
124 | if subscr[sid] then |
---|
125 | subscr[sid]=nil |
---|
126 | |
---|
127 | if cfg.debug>0 then print('unsubscribe: '..sid) end |
---|
128 | end |
---|
129 | end |
---|
130 | |
---|
131 | --store to cache |
---|
132 | function cache_store(k,v) |
---|
133 | local time=os.time() |
---|
134 | |
---|
135 | local cc=cache[k] |
---|
136 | |
---|
137 | if cc then cc.value=v cc.time=time return end |
---|
138 | |
---|
139 | if cache_size>=cfg.cache_size then |
---|
140 | local min_k=nil |
---|
141 | local min_time=nil |
---|
142 | for i,j in pairs(cache) do |
---|
143 | if not min_time or min_time>j.time then min_k=i min_time=j.time end |
---|
144 | end |
---|
145 | if min_k then |
---|
146 | if cfg.debug>0 then print('remove URL from cache (overflow): '..min_k) end |
---|
147 | cache[min_k]=nil |
---|
148 | cache_size=cache_size-1 |
---|
149 | end |
---|
150 | end |
---|
151 | |
---|
152 | local t={} |
---|
153 | t.time=time |
---|
154 | t.value=v |
---|
155 | cache[k]=t |
---|
156 | cache_size=cache_size+1 |
---|
157 | end |
---|
158 | |
---|
159 | |
---|
160 | -- garbage collection |
---|
161 | function sys_gc(what,sec) |
---|
162 | |
---|
163 | local t=os.time() |
---|
164 | |
---|
165 | -- force unsubscribe |
---|
166 | local g={} |
---|
167 | |
---|
168 | for i,j in pairs(subscr) do |
---|
169 | if os.difftime(t,j.timestamp)>=j.ttl then |
---|
170 | table.insert(g,i) |
---|
171 | end |
---|
172 | end |
---|
173 | |
---|
174 | for i,j in ipairs(g) do |
---|
175 | subscr[j]=nil |
---|
176 | |
---|
177 | if cfg.debug>0 then print('force unsubscribe (timeout): '..j) end |
---|
178 | end |
---|
179 | |
---|
180 | -- cache clear |
---|
181 | g={} |
---|
182 | |
---|
183 | for i,j in pairs(cache) do |
---|
184 | if os.difftime(t,j.time)>=cfg.cache_ttl then |
---|
185 | table.insert(g,i) |
---|
186 | end |
---|
187 | end |
---|
188 | |
---|
189 | cache_size=cache_size-table.maxn(g) |
---|
190 | |
---|
191 | for i,j in ipairs(g) do |
---|
192 | cache[j]=nil |
---|
193 | |
---|
194 | if cfg.debug>0 then print('remove URL from cache (timeout): '..j) end |
---|
195 | end |
---|
196 | |
---|
197 | core.timer(sec,what) |
---|
198 | end |
---|
199 | |
---|
200 | |
---|
201 | -- ContentDirectory event deliver (child process) |
---|
202 | function subscr_notify_iterate_tree(pls,tt) |
---|
203 | if pls.elements then |
---|
204 | table.insert(tt,pls.objid..','..update_id) |
---|
205 | |
---|
206 | for i,j in ipairs(pls.elements) do |
---|
207 | subscr_notify_iterate_tree(j,tt) |
---|
208 | end |
---|
209 | end |
---|
210 | end |
---|
211 | |
---|
212 | function subscr_notify_async(t) |
---|
213 | |
---|
214 | local tt={} |
---|
215 | subscr_notify_iterate_tree(playlist_data,tt) |
---|
216 | |
---|
217 | local data=string.format( |
---|
218 | '<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\"><e:property><SystemUpdateID>%s</SystemUpdateID><ContainerUpdateIDs>%s</ContainerUpdateIDs></e:property></e:propertyset>', |
---|
219 | update_id,table.concat(tt,',')) |
---|
220 | |
---|
221 | for i,j in ipairs(t) do |
---|
222 | if cfg.debug>0 then print('notify: '..j.callback..', sid='..j.sid..', seq='..j.seq) end |
---|
223 | http.notify(j.callback,j.sid,data,j.seq) |
---|
224 | end |
---|
225 | end |
---|
226 | |
---|
227 | |
---|
228 | -- reload all playlists |
---|
229 | function reload_playlist() |
---|
230 | reload_playlists() |
---|
231 | update_id=update_id+1 |
---|
232 | |
---|
233 | if update_id>100000 then update_id=1 end |
---|
234 | |
---|
235 | if cfg.debug>0 then print('reload playlist, update_id='..update_id) end |
---|
236 | |
---|
237 | if cfg.dlna_notify==true then |
---|
238 | local t={} |
---|
239 | |
---|
240 | for i,j in pairs(subscr) do |
---|
241 | if j.event=='cds' then |
---|
242 | table.insert(t, { ['callback']=j.callback, ['sid']=j.sid, ['seq']=j.seq } ) |
---|
243 | j.seq=j.seq+1 |
---|
244 | if j.seq>100000 then j.seq=0 end |
---|
245 | end |
---|
246 | end |
---|
247 | |
---|
248 | if table.maxn(t)>0 then |
---|
249 | core.fspawn(subscr_notify_async,t) |
---|
250 | end |
---|
251 | end |
---|
252 | end |
---|
253 | |
---|
254 | -- change child process status (for UI) |
---|
255 | function set_child_status(pid,status) |
---|
256 | pid=tonumber(pid) |
---|
257 | if childs[pid] then |
---|
258 | childs[pid].status=status |
---|
259 | childs[pid].time=os.time() |
---|
260 | end |
---|
261 | end |
---|
262 | |
---|
263 | function get_drive_state(drive) |
---|
264 | local s |
---|
265 | |
---|
266 | local f=io.popen('/sbin/hdparm -C '..drive..' 2>/dev/null | grep -i state','r') |
---|
267 | |
---|
268 | if f then |
---|
269 | s=f:read('*a') |
---|
270 | f:close() |
---|
271 | end |
---|
272 | |
---|
273 | return string.match(s,'drive state is:%s+(.+)%s+') |
---|
274 | end |
---|
275 | |
---|
276 | |
---|
277 | function profile_change(user_agent,req) |
---|
278 | if not user_agent or user_agent=='' then return end |
---|
279 | |
---|
280 | for name,profile in pairs(profiles) do |
---|
281 | local match=profile.match |
---|
282 | |
---|
283 | if profile.disabled~=true and match and match(user_agent,req) then |
---|
284 | |
---|
285 | local options=profile.options |
---|
286 | local mtypes=profile.mime_types |
---|
287 | |
---|
288 | if options then for i,j in pairs(options) do cfg[i]=j end end |
---|
289 | |
---|
290 | if mtypes then |
---|
291 | if profile.replace_mime_types==true then |
---|
292 | mime=mtypes |
---|
293 | else |
---|
294 | for i,j in pairs(mtypes) do mime[i]=j end |
---|
295 | end |
---|
296 | end |
---|
297 | |
---|
298 | return name |
---|
299 | end |
---|
300 | end |
---|
301 | return nil |
---|
302 | end |
---|
303 | |
---|
304 | |
---|
305 | -- event handlers |
---|
306 | events['SIGUSR1']=reload_playlist |
---|
307 | events['reload']=reload_playlist |
---|
308 | events['store']=cache_store |
---|
309 | events['sys_gc']=sys_gc |
---|
310 | events['subscribe']=subscribe |
---|
311 | events['unsubscribe']=unsubscribe |
---|
312 | events['update_feeds']=update_feeds |
---|
313 | events['status']=set_child_status |
---|
314 | events['config']=function() load_plugins(cfg.config_path,'config') cache={} cache_size=0 end |
---|
315 | events['remove_feed']=function(id) table.remove(feeds,tonumber(id)) end |
---|
316 | events['add_feed']=function(plugin,feed,name) table.insert(feeds,{[1]=plugin,[2]=feed,[3]=name}) end |
---|
317 | events['plugin']=function(name,status) if status=='on' then plugins[name].disabled=false else plugins[name].disabled=true end end |
---|
318 | events['profile']=function(name,status) if status=='on' then profiles[name].disabled=false else profiles[name].disabled=true end end |
---|
319 | events['bookmark']=function(objid,pos) local pls=find_playlist_object(objid) if pls then pls.bookmark=pos end end |
---|
320 | |
---|
321 | events['update_playlists']= |
---|
322 | function(what,sec) |
---|
323 | if cfg.drive and cfg.drive~='' then |
---|
324 | if get_drive_state(cfg.drive)=='active/idle' then |
---|
325 | reload_playlist() |
---|
326 | end |
---|
327 | else |
---|
328 | reload_playlist() |
---|
329 | end |
---|
330 | |
---|
331 | core.timer(cfg.playlists_update_interval,what) |
---|
332 | end |
---|
333 | |
---|
334 | |
---|
335 | if cfg.embedded==true then print=function () end end |
---|
336 | |
---|
337 | -- start garbage collection system |
---|
338 | core.timer(300,'sys_gc') |
---|
339 | |
---|
340 | http.timeout(cfg.http_timeout) |
---|
341 | http.user_agent(cfg.user_agent) |
---|
342 | |
---|
343 | -- start feeds update system |
---|
344 | if cfg.feeds_update_interval>0 then |
---|
345 | core.timer(15,'update_feeds') |
---|
346 | end |
---|
347 | |
---|
348 | if cfg.playlists_update_interval>0 then |
---|
349 | core.timer(cfg.playlists_update_interval,'update_playlists') |
---|
350 | end |
---|
351 | |
---|
352 | load_plugins(cfg.config_path..'postinit/','postinit') |
---|
353 | |
---|
354 | print("start "..cfg.log_ident) |
---|
355 | |
---|
356 | core.mainloop() |
---|
357 | |
---|
358 | print("stop "..cfg.log_ident) |
---|
359 | |
---|
360 | if cfg.daemon==true then os.execute('rm -f '..cfg.pid_file) end |
---|