autotimer/AutoTimerResource: handle exceptions in background better
[enigma2-plugins.git] / autotimer / src / AutoTimerResource.py
1 # -*- coding: UTF-8 -*-
2 from AutoTimer import AutoTimer
3 from AutoTimerConfiguration import CURRENT_CONFIG_VERSION
4 from RecordTimer import AFTEREVENT
5 from twisted.internet import reactor
6 from twisted.web import http, resource, server
7 import threading
8 try:
9         from urllib import unquote
10 except ImportError as ie:
11         from urllib.parse import unquote
12 from ServiceReference import ServiceReference
13 from Tools.XMLTools import stringToXML
14 from enigma import eServiceReference
15 from . import _, config, iteritems, plugin
16 from plugin import autotimer
17
18 API_VERSION = "1.3"
19
20 class AutoTimerBaseResource(resource.Resource):
21         def returnResult(self, req, state, statetext):
22                 req.setResponseCode(http.OK)
23                 req.setHeader('Content-type', 'application/xhtml+xml')
24                 req.setHeader('charset', 'UTF-8')
25
26                 return """<?xml version=\"1.0\" encoding=\"UTF-8\" ?>
27 <e2simplexmlresult>
28         <e2state>%s</e2state>
29         <e2statetext>%s</e2statetext>
30 </e2simplexmlresult>""" % ('True' if state else 'False', statetext)
31
32 class AutoTimerBackgroundThread(threading.Thread):
33         def __init__(self, req, fnc):
34                 threading.Thread.__init__(self)
35                 self._req = req
36                 if hasattr(req, 'notifyFinish'):
37                         req.notifyFinish().addErrback(self.connectionLost)
38                 self._stillAlive = True
39                 self._fnc = fnc
40                 self.start()
41
42         def connectionLost(self, err):
43                 self._stillAlive = False
44
45         def run(self):
46                 req = self._req
47                 code = http.OK
48                 try: ret = self._fnc(req)
49                 except Exception as e:
50                         ret = str(e)
51                         code = http.INTERNAL_SERVER_ERROR
52                 def finishRequest():
53                         req.setResponseCode(code)
54                         if code == http.OK:
55                                 req.setHeader('Content-type', 'application/xhtml+xml')
56                         req.setHeader('charset', 'UTF-8')
57                         req.write(ret)
58                         req.finish()
59                 if self._stillAlive:
60                         reactor.callFromThread(finishRequest)
61
62 class AutoTimerBackgroundingResource(AutoTimerBaseResource, threading.Thread):
63         def render(self, req):
64                 AutoTimerBackgroundThread(req, self.renderBackground)
65                 return server.NOT_DONE_YET
66
67         def renderBackground(self, req):
68                 pass
69
70 class AutoTimerDoParseBackgroundThread(AutoTimerBackgroundThread):
71         def run(self):
72                 req = self._req
73                 if self._stillAlive:
74                         req.setResponseCode(http.OK)
75                         req.setHeader('Content-type', 'application/xhtml+xml')
76                         req.setHeader('charset', 'UTF-8')
77                         reactor.callFromThread(lambda: req.write("""<?xml version=\"1.0\" encoding=\"UTF-8\" ?><e2simplexmlresult>"""))
78
79                 d = autotimer.parseEPGAsync().addCallback(self.epgCallback).addErrback(self.epgErrback)
80                 def timeout():
81                         if not d.called and self._stillAlive:
82                                 reactor.callFromThread(lambda: req.write("<ignore />"))
83                                 reactor.callLater(50, timeout)
84                 reactor.callLater(50, timeout)
85
86         def epgCallback(self, ret):
87                 if self._stillAlive:
88                         ret = """<e2state>True</e2state>
89         <e2statetext>"""+ _("Found a total of %d matching Events.\n%d Timer were added and\n%d modified,\n%d conflicts encountered,\n%d similars added.") % (ret[0], ret[1], ret[2], len(ret[4]), len(ret[5])) + "</e2statetext></e2simplexmlresult>"
90                         def finishRequest():
91                                 self._req.write(ret)
92                                 self._req.finish()
93                         reactor.callFromThread(finishRequest)
94
95         def epgErrback(self, failure):
96                 if self._stillAlive:
97                         ret = """<e2state>False</e2state>
98         <e2statetext>"""+ _("AutoTimer failed with error %s") % (str(failure),) + "</e2statetext></e2simplexmlresult>"
99                         def finishRequest():
100                                 self._req.write(ret)
101                                 self._req.finish()
102                         reactor.callFromThread(finishRequest)
103
104 class AutoTimerDoParseResource(AutoTimerBaseResource):
105         def render(self, req):
106                 AutoTimerDoParseBackgroundThread(req, None)
107                 return server.NOT_DONE_YET
108
109 class AutoTimerSimulateBackgroundThread(AutoTimerBackgroundThread):
110         def run(self):
111                 req = self._req
112                 if self._stillAlive:
113                         req.setResponseCode(http.OK)
114                         req.setHeader('Content-type', 'application/xhtml+xml')
115                         req.setHeader('charset', 'UTF-8')
116                         reactor.callFromThread(lambda: req.write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<e2autotimersimulate api_version=\"" + str(API_VERSION) + "\">\n"))
117
118                 def finishRequest():
119                         req.write('</e2autotimersimulate>')
120                         req.finish()
121
122                 try: autotimer.parseEPG(simulateOnly=True, callback=self.intermediateWrite)
123                 except Exception as e:
124                         def finishRequest():
125                                 req.write('<exception>'+str(e)+'</exception><|PURPOSEFULLYBROKENXML<')
126                                 req.finish()
127
128                 if self._stillAlive:
129                         reactor.callFromThread(finishRequest)
130
131         def intermediateWrite(self, new, conflicting, similar):
132                 returnlist = []
133                 extend = returnlist.extend
134
135                 for (name, begin, end, serviceref, autotimername) in new:
136                         ref = ServiceReference(str(serviceref))
137                         extend((
138                                 '<e2simulatedtimer>\n'
139                                 '   <e2servicereference>', stringToXML(serviceref), '</e2servicereference>\n',
140                                 '   <e2servicename>', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), '</e2servicename>\n',
141                                 '   <e2name>', stringToXML(name), '</e2name>\n',
142                                 '   <e2timebegin>', str(begin), '</e2timebegin>\n',
143                                 '   <e2timeend>', str(end), '</e2timeend>\n',
144                                 '   <e2autotimername>', stringToXML(autotimername), '</e2autotimername>\n'
145                                 '</e2simulatedtimer>\n'
146                         ))
147
148                 if self._stillAlive:
149                         reactor.callFromThread(lambda: self._req.write(''.join(returnlist)))
150
151 class AutoTimerSimulateResource(AutoTimerBaseResource):
152         def render(self, req):
153                 AutoTimerSimulateBackgroundThread(req, None)
154                 return server.NOT_DONE_YET
155
156 class AutoTimerListAutoTimerResource(AutoTimerBaseResource):
157         def render(self, req):
158                 # We re-read the config so we won't display empty or wrong information
159                 try:
160                         autotimer.readXml()
161                 except Exception as e:
162                         return self.returnResult(req, False, _("Couldn't load config file!") + '\n' + str(e))
163
164                 # show xml
165                 req.setResponseCode(http.OK)
166                 req.setHeader('Content-type', 'application/xhtml+xml')
167                 req.setHeader('charset', 'UTF-8')
168                 return ''.join(autotimer.getXml())
169
170 class AutoTimerRemoveAutoTimerResource(AutoTimerBaseResource):
171         def render(self, req):
172                 id = req.args.get("id")
173                 if id:
174                         autotimer.remove(int(id[0]))
175                         return self.returnResult(req, True, _("AutoTimer was removed"))
176                 else:
177                         return self.returnResult(req, False, _("missing parameter \"id\""))
178
179 class AutoTimerAddOrEditAutoTimerResource(AutoTimerBaseResource):
180         # TODO: recheck if we can modify regular config parser to work on this
181         # TODO: allow to edit defaults?
182         def render(self, req):
183                 def get(name, default=None):
184                         ret = req.args.get(name)
185                         return ret[0] if ret else default
186
187                 id = get("id")
188                 timer = None
189                 newTimer = True
190                 if id is None:
191                         id = autotimer.getUniqueId()
192                         timer = autotimer.defaultTimer.clone()
193                         timer.id = id
194                 else:
195                         id = int(id)
196                         for possibleMatch in autotimer.getTimerList():
197                                 if possibleMatch.id == id:
198                                         timer = possibleMatch
199                                         newTimer = False
200                                         break
201                         if timer is None:
202                                 return self.returnResult(req, False, _("unable to find timer with id %i" % (id,)))
203
204                 if id != -1:
205                         # Match
206                         timer.match = unquote(get("match", timer.match))
207                         if not timer.match:
208                                 return self.returnResult(req, False, _("autotimers need a match attribute"))
209
210                         # Name
211                         timer.name = unquote(get("name", timer.name)).strip()
212                         if not timer.name: timer.name = timer.match
213
214                         # Enabled
215                         enabled = get("enabled")
216                         if enabled is not None:
217                                 try: enabled = int(enabled)
218                                 except ValueError: enabled = enabled == "yes"
219                                 timer.enabled = enabled
220
221                         # Timeframe
222                         before = get("before")
223                         after = get("after")
224                         if before and after:
225                                 timer.timeframe = (int(after), int(before))
226                         elif before == '' or after == '':
227                                 timer.timeframe = None
228
229                 # ...
230                 timer.searchType = get("searchType", timer.searchType)
231                 timer.searchCase = get("searchCase", timer.searchCase)
232
233                 # Alternatives
234                 timer.overrideAlternatives = int(get("overrideAlternatives", timer.overrideAlternatives))
235
236                 # Justplay
237                 justplay = get("justplay")
238                 if justplay is not None:
239                         try: justplay = int(justplay)
240                         except ValueError: justplay = justplay == "zap"
241                         timer.justplay = justplay
242                 setEndtime = get("setEndtime")
243                 if setEndtime is not None:
244                         timer.setEndtime = int(setEndtime)
245
246                 # Timespan
247                 start = get("timespanFrom")
248                 end = get("timespanTo")
249                 if start and end:
250                         start = [int(x) for x in start.split(':')]
251                         end = [int(x) for x in end.split(':')]
252                         timer.timespan = (start, end)
253                 elif start == '' and end == '':
254                         timer.timespan = None
255
256                 # Services
257                 servicelist = get("services")
258                 if servicelist is not None:
259                         servicelist = unquote(servicelist).split(',')
260                         appendlist = []
261                         for value in servicelist:
262                                 myref = eServiceReference(str(value))
263                                 if not (myref.flags & eServiceReference.isGroup):
264                                         # strip all after last :
265                                         pos = value.rfind(':')
266                                         if pos != -1:
267                                                 if value[pos-1] == ':':
268                                                         pos -= 1
269                                                 value = value[:pos+1]
270
271                                 if myref.valid():
272                                         appendlist.append(value)
273                         timer.services = appendlist
274
275                 # Bouquets
276                 servicelist = get("bouquets")
277                 if servicelist is not None:
278                         servicelist = unquote(servicelist).split(',')
279                         while '' in servicelist: servicelist.remove('')
280                         timer.bouquets = servicelist
281
282                 # Offset
283                 offset = get("offset")
284                 if offset:
285                         offset = offset.split(',')
286                         if len(offset) == 1:
287                                 before = after = int(offset[0] or 0) * 60
288                         else:
289                                 before = int(offset[0] or 0) * 60
290                                 after = int(offset[1] or 0) * 60
291                         timer.offset = (before, after)
292                 elif offset == '':
293                         timer.offset = None
294
295                 # AfterEvent
296                 afterevent = get("afterevent")
297                 if afterevent:
298                         if afterevent == "default":
299                                 timer.afterevent = []
300                         else:
301                                 try: afterevent = int(afterevent)
302                                 except ValueError:
303                                         afterevent = {
304                                                 "nothing": AFTEREVENT.NONE,
305                                                 "deepstandby": AFTEREVENT.DEEPSTANDBY,
306                                                 "standby": AFTEREVENT.STANDBY,
307                                                 "auto": AFTEREVENT.AUTO
308                                         }.get(afterevent, AFTEREVENT.AUTO)
309                                 start = get("aftereventFrom")
310                                 end = get("aftereventTo")
311                                 if start and end:
312                                         start = [int(x) for x in start.split(':')]
313                                         end = [int(x) for x in end.split(':')]
314                                         timer.afterevent = [(afterevent, (start, end))]
315                                 else:
316                                         timer.afterevent = [(afterevent, None)]
317
318                 # Maxduration
319                 maxduration = get("maxduration")
320                 if maxduration:
321                         timer.maxduration = int(maxduration)*60
322                 elif maxduration == '':
323                         timer.maxduration = None
324
325                 # Includes
326                 title = req.args.get("title")
327                 shortdescription = req.args.get("shortdescription")
328                 description = req.args.get("description")
329                 dayofweek = req.args.get("dayofweek")
330                 if title or shortdescription or description or dayofweek:
331                         includes = timer.include
332                         title = [unquote(x) for x in title] if title else includes[0]
333                         shortdescription = [unquote(x) for x in shortdescription] if shortdescription else includes[1]
334                         description = [unquote(x) for x in description] if description else includes[2]
335                         dayofweek = [unquote(x) for x in dayofweek] if dayofweek else includes[3]
336                         while '' in title: title.remove('')
337                         while '' in shortdescription: shortdescription.remove('')
338                         while '' in description: description.remove('')
339                         while '' in dayofweek: dayofweek.remove('')
340                         timer.include = (title, shortdescription, description, dayofweek)
341
342                 # Excludes
343                 title = req.args.get("!title")
344                 shortdescription = req.args.get("!shortdescription")
345                 description = req.args.get("!description")
346                 dayofweek = req.args.get("!dayofweek")
347                 if title or shortdescription or description or dayofweek:
348                         excludes = timer.exclude
349                         title = [unquote(x) for x in title] if title else excludes[0]
350                         shortdescription = [unquote(x) for x in shortdescription] if shortdescription else excludes[1]
351                         description = [unquote(x) for x in description] if description else excludes[2]
352                         dayofweek = [unquote(x) for x in dayofweek] if dayofweek else excludes[3]
353                         while '' in title: title.remove('')
354                         while '' in shortdescription: shortdescription.remove('')
355                         while '' in description: description.remove('')
356                         while '' in dayofweek: dayofweek.remove('')
357                         timer.exclude = (title, shortdescription, description, dayofweek)
358
359                 tags = req.args.get("tag")
360                 if tags:
361                         while '' in tags: tags.remove('')
362                         timer.tags = [unquote(x) for x in tags]
363
364                 timer.matchCount = int(get("counter", timer.matchCount))
365                 timer.matchFormatString = get("counterFormat", timer.matchFormatString)
366                 if id != -1:
367                         matchLeft = get("left")
368                         timer.matchLeft = int(matchLeft) if matchLeft else (timer.matchCount if newTimer else timer.matchLeft)
369                         timer.matchLimit = get("lastActivation", timer.matchLimit)
370                         timer.lastBegin = int(get("lastBegin", timer.lastBegin))
371
372                 timer.avoidDuplicateDescription = int(get("avoidDuplicateDescription", timer.avoidDuplicateDescription))
373                 timer.searchForDuplicateDescription = int(get("searchForDuplicateDescription", timer.searchForDuplicateDescription))
374                 timer.destination = get("location", timer.destination) or None
375
376                 # vps
377                 enabled = get("vps_enabled")
378                 if enabled is not None:
379                         try: enabled = int(enabled)
380                         except ValueError: enabled = enabled == "yes"
381                         timer.vps_enabled = enabled
382                 vps_overwrite = get("vps_overwrite")
383                 if vps_overwrite is not None:
384                         try: vps_overwrite = int(vps_overwrite)
385                         except ValueError: vps_overwrite = vps_overwrite == "yes"
386                         timer.vps_overwrite = vps_overwrite
387                 if not timer.vps_enabled and timer.vps_overwrite:
388                         timer.vps_overwrite = False
389
390                 # SeriesPlugin
391                 series_labeling = get("series_labeling")
392                 if series_labeling is not None:
393                         try: series_labeling = int(series_labeling)
394                         except ValueError: series_labeling = series_labeling == "yes"
395                         timer.series_labeling = series_labeling
396
397                 if newTimer:
398                         autotimer.add(timer)
399                         message = _("AutoTimer was added successfully")
400                 else:
401                         message = _("AutoTimer was changed successfully")
402
403                 return self.returnResult(req, True, message)
404
405 class AutoTimerChangeSettingsResource(AutoTimerBaseResource):
406         def render(self, req):
407                 for key, value in iteritems(req.args):
408                         value = value[0]
409                         if key == "autopoll":
410                                 config.plugins.autotimer.autopoll.value = True if value == "true" else False
411                         elif key == "interval":
412                                 config.plugins.autotimer.interval.value = int(value)
413                         elif key == "refresh":
414                                 config.plugins.autotimer.refresh.value = value
415                         elif key == "try_guessing":
416                                 config.plugins.autotimer.try_guessing.value = True if value == "true" else False
417                         elif key == "editor":
418                                 config.plugins.autotimer.editor.value = value
419                         elif key == "disabled_on_conflict":
420                                 config.plugins.autotimer.disabled_on_conflict.value = True if value == "true" else False
421                         elif key == "addsimilar_on_conflict":
422                                 config.plugins.autotimer.addsimilar_on_conflict.value = True if value == "true" else False
423                         elif key == "show_in_extensionsmenu":
424                                 config.plugins.autotimer.show_in_extensionsmenu.value = True if value == "true" else False
425                         elif key == "fastscan":
426                                 config.plugins.autotimer.fastscan.value = True if value == "true" else False
427                         elif key == "notifconflict":
428                                 config.plugins.autotimer.notifconflict.value = True if value == "true" else False
429                         elif key == "notifsimilar":
430                                 config.plugins.autotimer.notifsimilar.value = True if value == "true" else False
431                         elif key == "maxdaysinfuture":
432                                 config.plugins.autotimer.maxdaysinfuture.value = int(value)
433                         elif key == "add_autotimer_to_tags":
434                                 config.plugins.autotimer.add_autotimer_to_tags.value = True if value == "true" else False
435                         elif key == "add_name_to_tags":
436                                 config.plugins.autotimer.add_name_to_tags.value = True if value == "true" else False
437
438                 if config.plugins.autotimer.autopoll.value:
439                         if plugin.autopoller is None:
440                                 from AutoPoller import AutoPoller
441                                 plugin.autopoller = AutoPoller()
442                         plugin.autopoller.start(initial = False)
443                 else:
444                         if plugin.autopoller is not None:
445                                 plugin.autopoller.stop()
446                                 plugin.autopoller = None
447
448                 return self.returnResult(req, True, _("config changed."))
449
450 class AutoTimerSettingsResource(resource.Resource):
451         def render(self, req):
452                 req.setResponseCode(http.OK)
453                 req.setHeader('Content-type', 'application/xhtml+xml')
454                 req.setHeader('charset', 'UTF-8')
455
456                 try:
457                         from Plugins.SystemPlugins.vps import Vps
458                 except ImportError as ie:
459                         hasVps = False
460                 else:
461                         hasVps = True
462
463                 try:
464                         from Plugins.Extensions.SeriesPlugin.plugin import Plugins
465                 except ImportError as ie:
466                         hasSeriesPlugin = False
467                 else:
468                         hasSeriesPlugin = True
469
470                 return """<?xml version=\"1.0\" encoding=\"UTF-8\" ?>
471 <e2settings>
472         <e2setting>
473                 <e2settingname>config.plugins.autotimer.autopoll</e2settingname>
474                 <e2settingvalue>%s</e2settingvalue>
475         </e2setting>
476         <e2setting>
477                 <e2settingname>config.plugins.autotimer.interval</e2settingname>
478                 <e2settingvalue>%d</e2settingvalue>
479         </e2setting>
480         <e2setting>
481                 <e2settingname>config.plugins.autotimer.refresh</e2settingname>
482                 <e2settingvalue>%s</e2settingvalue>
483         </e2setting>
484         <e2setting>
485                 <e2settingname>config.plugins.autotimer.try_guessing</e2settingname>
486                 <e2settingvalue>%s</e2settingvalue>
487         </e2setting>
488         <e2setting>
489                 <e2settingname>config.plugins.autotimer.editor</e2settingname>
490                 <e2settingvalue>%s</e2settingvalue>
491         </e2setting>
492         <e2setting>
493                 <e2settingname>config.plugins.autotimer.disabled_on_conflict</e2settingname>
494                 <e2settingvalue>%s</e2settingvalue>
495         </e2setting>
496         <e2setting>
497                 <e2settingname>config.plugins.autotimer.addsimilar_on_conflict</e2settingname>
498                 <e2settingvalue>%s</e2settingvalue>
499         </e2setting>
500         <e2setting>
501                 <e2settingname>config.plugins.autotimer.show_in_extensionsmenu</e2settingname>
502                 <e2settingvalue>%s</e2settingvalue>
503         </e2setting>
504         <e2setting>
505                 <e2settingname>config.plugins.autotimer.fastscan</e2settingname>
506                 <e2settingvalue>%s</e2settingvalue>
507         </e2setting>
508         <e2setting>
509                 <e2settingname>config.plugins.autotimer.notifconflict</e2settingname>
510                 <e2settingvalue>%s</e2settingvalue>
511         </e2setting>
512         <e2setting>
513                 <e2settingname>config.plugins.autotimer.notifsimilar</e2settingname>
514                 <e2settingvalue>%s</e2settingvalue>
515         </e2setting>
516         <e2setting>
517                 <e2settingname>config.plugins.autotimer.maxdaysinfuture</e2settingname>
518                 <e2settingvalue>%s</e2settingvalue>
519         </e2setting>
520         <e2setting>
521                 <e2settingname>config.plugins.autotimer.add_autotimer_to_tags</e2settingname>
522                 <e2settingvalue>%s</e2settingvalue>
523         </e2setting>
524         <e2setting>
525                 <e2settingname>config.plugins.autotimer.add_name_to_tags</e2settingname>
526                 <e2settingvalue>%s</e2settingvalue>
527         </e2setting>
528         <e2setting>
529                 <e2settingname>hasVps</e2settingname>
530                 <e2settingvalue>%s</e2settingvalue>
531         </e2setting>
532         <e2setting>
533                 <e2settingname>hasSeriesPlugin</e2settingname>
534                 <e2settingvalue>%s</e2settingvalue>
535         </e2setting>
536         <e2setting>
537                 <e2settingname>version</e2settingname>
538                 <e2settingvalue>%s</e2settingvalue>
539         </e2setting>
540         <e2setting>
541                 <e2settingname>api_version</e2settingname>
542                 <e2settingvalue>%s</e2settingvalue>
543         </e2setting>
544 </e2settings>""" % (
545                                 config.plugins.autotimer.autopoll.value,
546                                 config.plugins.autotimer.interval.value,
547                                 config.plugins.autotimer.refresh.value,
548                                 config.plugins.autotimer.try_guessing.value,
549                                 config.plugins.autotimer.editor.value,
550                                 config.plugins.autotimer.addsimilar_on_conflict.value,
551                                 config.plugins.autotimer.disabled_on_conflict.value,
552                                 config.plugins.autotimer.show_in_extensionsmenu.value,
553                                 config.plugins.autotimer.fastscan.value,
554                                 config.plugins.autotimer.notifconflict.value,
555                                 config.plugins.autotimer.notifsimilar.value,
556                                 config.plugins.autotimer.maxdaysinfuture.value,
557                                 config.plugins.autotimer.add_autotimer_to_tags.value,
558                                 config.plugins.autotimer.add_name_to_tags.value,
559                                 hasVps,
560                                 hasSeriesPlugin,
561                                 CURRENT_CONFIG_VERSION,
562                                 API_VERSION,
563                         )