update some syntax and make the code more future-proof ;)
[enigma2-plugins.git] / svdrp / src / SVDRP.py
1 from __future__ import print_function
2
3 from twisted.internet.defer import Deferred
4 from twisted.internet.protocol import ClientFactory, ServerFactory
5 from twisted.internet import reactor
6 from twisted.protocols.basic import LineReceiver
7 from Screens.InfoBar import InfoBar
8 from enigma import eEPGCache, eDVBVolumecontrol, eServiceCenter, eServiceReference, iServiceInformation
9 from ServiceReference import ServiceReference
10 from Components.TimerSanityCheck import TimerSanityCheck
11 from RecordTimer import RecordTimerEntry
12
13 from Screens.MessageBox import MessageBox
14 from Tools import Notifications
15 from time import localtime, mktime, strftime, strptime
16 from os import uname
17
18 VERSION = '0.1'
19 SVDRP_TCP_PORT = 6419
20 NOTIFICATIONID = 'SVDRPNotification'
21
22 CODE_HELP = 214
23 CODE_EPG = 215
24 CODE_IMAGE = 216
25 CODE_HELO = 220
26 CODE_BYE = 221
27 CODE_OK = 250
28 CODE_EPG_START = 354 
29 CODE_ERR_LOCAL = 451
30 CODE_UNK = 500
31 CODE_SYNTAX = 501 
32 CODE_IMP_FUNC = 502
33 CODE_IMP_PARAM = 504
34 CODE_NOK = 550
35 CODE_ERR = 554
36 class SimpleVDRProtocol(LineReceiver):
37         def __init__(self, client = False):
38                 self.client = client
39                 self._channelList = []
40                 from Components.MovieList import MovieList
41                 from Tools.Directories import resolveFilename, SCOPE_HDD
42                 self.movielist = MovieList(eServiceReference("2:0:1:0:0:0:0:0:0:0:" + resolveFilename(SCOPE_HDD)))
43
44         def getChannelList(self):
45                 if not self._channelList:
46                         from Components.Sources.ServiceList import ServiceList
47                         bouquet = eServiceReference('1:7:1:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.favourites.tv" ORDER BY bouquet')
48                         slist = ServiceList(bouquet, validate_commands=False)
49                         services = slist.getServicesAsList(format="S")
50                         self._channelList = services[:]
51                 return self._channelList
52
53         def setChannelList(self, channelList):
54                 self._channelList = channelList
55
56         channelList = property(getChannelList, setChannelList)
57
58         def connectionMade(self):
59                 self.factory.addClient(self)
60                 now = strftime('%a %b %d %H:%M:%S %Y', localtime())
61                 payload = "%d %s SVDRP VideoDiskRecorder (Enigma 2-Plugin %s); %s" % (CODE_HELO, uname()[1], VERSION, now)
62                 self.sendLine(payload)
63
64         def connectionLost(self, reason):
65                 self.factory.removeClient(self)
66
67         def stop(self, *args):
68                 payload = "%d %s closing connection" % (CODE_BYE, uname()[1])
69                 self.sendLine(payload)
70                 self.transport.loseConnection()
71
72         def NOT_IMPLEMENTED(self, args):
73                 print("[SVDRP] command not implemented.")
74                 payload = "%d command not implemented." % (CODE_IMP_FUNC,)
75                 self.sendLine(payload)
76
77         def CHAN(self, args):
78                 # allowed parameters: [ + | - | <number> | <name> | <id> ]
79                 if args == '+':
80                         InfoBar.instance.zapDown()
81                         payload = "%d channel changed" % (CODE_OK,)
82                 elif args == '-':
83                         InfoBar.instance.zapUp()
84                         payload = "%d channel changed" % (CODE_OK,)
85                 else:
86                         # can be number, name or id
87                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
88
89                 self.sendLine(payload)
90
91         def LSTC(self, args):
92                 if args:
93                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
94                         return self.sendLine(payload)
95                 from Components.Sources.ServiceList import ServiceList
96                 bouquet = eServiceReference('1:7:1:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.favourites.tv" ORDER BY bouquet')
97                 slist = ServiceList(bouquet, validate_commands=False)
98                 services = slist.getServicesAsList(format="SNn")
99                 if services:
100                         def getServiceInfoValue(info, sref, what):
101                                 if info is None: return ""
102                                 v = info.getInfo(sref.ref, what)
103                                 if v == -2: return info.getInfoString(sref.ref, what)
104                                 elif v == -1: return "N/A"
105                                 return v
106                         def sendServiceLine(service, counter, last=False):
107                                 if service[0][:5] == '1:64:':
108                                         # format for markers:  ":Name"
109                                         line = "%d%s:%s" % (CODE_OK, '-' if not last else ' ', service[1])
110                                 else:
111                                         # <id> <full name>,<short name>;<provider>:<freq>:<parameters>:<source>:<srate>:<vpid>:<apid>:<tpid>:<conditional access>:<:sid>:<nid>:<tid>:<:rid>
112                                         # e.g. 5  RTL Television,RTL:12188:h:S19.2E:27500:163:104:105:0:12003:1:1089:0
113                                         sref = ServiceReference(service[0])
114                                         info = sref.info()
115                                         # XXX: how to get this?! o0
116                                         feinfo = None #sref.ref.frontendInfo()
117                                         fedata = feinfo.getAll(True) if feinfo else {}
118                                         prov = getServiceInfoValue(info, sref, iServiceInformation.sProvider)
119                                         frequency = fedata.get("frequency", 0)/1000
120                                         param = -1
121                                         source = '-1'
122                                         srate = -1
123                                         vpid = '-1'
124                                         apid = '-1'
125                                         tpid = -1
126                                         ca = '-1'
127                                         sid = -1
128                                         nid = -1
129                                         tid = -1
130                                         rid = -1
131                                         # TODO: support full format, these are only the important fields ;)
132                                         line = "%d%s%d %s,%s;%s:%d:%s:%s:%d:%s:%s:%d:%s:%d:%d:%d:%d" % (CODE_OK, '-' if not last else ' ', counter, service[1], service[2], prov, frequency, param, source, srate, vpid, apid, tpid, ca, sid, nid, tid, rid)
133                                 self.sendLine(line)
134
135                         self.channelList = [x[0] for x in services] # always refresh cache b/c this is what the user works with from now on
136                         lastItem = services.pop()
137                         idx = 1
138                         for service in services:
139                                 sendServiceLine(service, idx)
140                                 idx += 1
141                         sendServiceLine(lastItem, idx, last=True)
142                 else:
143                         payload = "%d no services found" % (CODE_ERR_LOCAL,)
144                         self.sendLine(payload)
145
146         def sendTimerLine(self, timer, counter, last=False):
147                 # <number> <flags>:<channel id>:<YYYY-MM-DD>:<HHMM>:<HHMM>:<priority>:<lifetime>:<name>:<auxiliary>
148                 flags = 0
149                 if not timer.disabled: flags |= 1
150                 if timer.state == timer.StateRunning: flags |= 8
151                 try:
152                         channelid = self.channelList.index(str(timer.service_ref)) + 1
153                 except ValueError as e:
154                         # XXX: ignore timers on channels that are not in our favourite bouquet
155                         return False
156                 else:
157                         datestring = strftime('%Y-%m-%d', localtime(timer.begin))
158                         beginstring = strftime('%H%M', localtime(timer.begin))
159                         endstring = strftime('%H%M', localtime(timer.end))
160                         line = "%d%s%d %d:%d:%s:%s:%s:%d:%d:%s:%s" % (CODE_OK, '-' if not last else ' ', counter, flags, channelid, datestring, beginstring, endstring, 1, 1, timer.name, timer.description)
161                         self.sendLine(line)
162                         return True
163
164         def LSTT(self, args):
165                 import NavigationInstance
166                 list = []
167                 recordTimer = NavigationInstance.instance.RecordTimer
168                 list.extend(recordTimer.timer_list)
169                 list.extend(recordTimer.processed_timers)
170                 list.sort(cmp = lambda x, y: x.begin < y.begin)
171
172                 lastItem = list.pop()
173                 idx = 1
174                 for timer in list:
175                         self.sendTimerLine(timer, idx)
176                         idx += 1
177                 if not self.sendTimerLine(lastItem, idx, last=True):
178                         # send error if last item failed to send, else the other end might get stuck
179                         payload = "%d data inconsistency error." % (CODE_ERR_LOCAL,)
180                         self.sendLine(payload)
181
182         def UPDT(self, args):
183                 # <id> <settings>
184                 args = args.split(None, 1)
185                 if len(args) != 2:
186                         payload = "%d argument error" % (CODE_SYNTAX,)
187                         return self.sendLine(payload)
188
189                 try:
190                         timerId = int(args[0])
191                 except ValueError:
192                         payload = "%d argument error" % (CODE_SYNTAX,)
193                         return self.sendLine(payload)
194
195                 import NavigationInstance
196                 list = []
197                 recordTimer = NavigationInstance.instance.RecordTimer
198                 list.extend(recordTimer.timer_list)
199                 list.extend(recordTimer.processed_timers)
200                 list.sort(cmp = lambda x, y: x.begin < y.begin)
201
202                 if timerId < 1:
203                         payload = "%d argument error" % (CODE_SYNTAX,)
204                         return self.sendLine(payload)
205
206                 if len(list) >= timerId: oldTimer = list[timerId - 1]
207                 else: oldTimer = None
208
209                 try:
210                         flags, channelid, datestring, beginstring, endstring, priority, lifetime, name, description = args[1].split(':')
211                         flags = int(flags)
212                         service_ref = ServiceReference(self.channelList[int(channelid)-1])
213                         datestruct = strptime(datestring, '%Y-%m-%d')
214                         timestruct = strptime(beginstring, '%H%M')
215                         begin = mktime((datestruct.tm_year, datestruct.tm_mon, datestruct.tm_mday, timestruct.tm_hour, timestruct.tm_min, 0, datestruct.tm_wday, datestruct.tm_yday, -1))
216                         timestruct = strptime(endstring, '%H%M')
217                         end = mktime((datestruct.tm_year, datestruct.tm_mon, datestruct.tm_mday, timestruct.tm_hour, timestruct.tm_min, 0, datestruct.tm_wday, datestruct.tm_yday, -1))
218                         del datestruct, timestruct
219                 except ValueError as e:
220                         payload = "%d argument error" % (CODE_SYNTAX,)
221                         return self.sendLine(payload)
222                 except KeyError as e:
223                         payload = "%d argument error" % (CODE_SYNTAX,)
224                         return self.sendLine(payload)
225
226                 if end < begin: end += 86400 # Add 1 day, beware - this is evil and might not work correctly due to dst
227                 timer = RecordTimerEntry(service_ref, begin, end, name, description, 0, disabled=flags & 1 == 0)
228                 if oldTimer:
229                         recordTimer.removeEntry(oldTimer)
230                         timer.justplay = oldTimer.justplay
231                         timer.afterEvent = oldTimer.afterEvent
232                         timer.dirname = oldTimer.dirname
233                         timer.tags = oldTimer.tags
234                         timer.log_entries = oldTimer.log_entries
235
236                 conflict = recordTimer.record(timer)
237                 if conflict is None:
238                         return self.sendTimerLine(timer, timerId, last=True)
239                 else:
240                         payload = "%d timer conflict detected, original timer lost." % (CODE_ERR_LOCAL,)
241                         return self.sendLine(payload)
242
243         def NEWT(self, args):
244                 self.UPDT("999999999 " + args)
245
246         def MODT(self, args):
247                 # <id> on | off | <settings>
248                 args = args.split(None, 1)
249                 if len(args) != 2:
250                         payload = "%d argument error" % (CODE_SYNTAX,)
251                         return self.sendLine(payload)
252
253                 if args[1] in ('on', 'off'):
254                         try:
255                                 timerId = int(args[0])
256                         except ValueError:
257                                 payload = "%d argument error" % (CODE_SYNTAX,)
258                                 return self.sendLine(payload)
259
260                         import NavigationInstance
261                         list = []
262                         recordTimer = NavigationInstance.instance.RecordTimer
263                         list.extend(recordTimer.timer_list)
264                         list.extend(recordTimer.processed_timers)
265                         list.sort(cmp = lambda x, y: x.begin < y.begin)
266
267                         if timerId < 1 or len(list) < timerId:
268                                 payload = "%d argument error" % (CODE_SYNTAX,)
269                                 return self.sendLine(payload)
270
271                         timer = list[timerId - 1]
272                         disable = args[1] == 'off'
273                         if disable and timer.isRunning():
274                                 payload = "%d timer is running, not disabling." % (CODE_ERR_LOCAL,)
275                                 return self.sendLine(payload)
276                         else:
277                                 if timer.disabled and not disable:
278                                         timer.enable()
279                                         tsc = TimerSanityCheck(recordTimer.timer_list, timer)
280                                         if not timersanitycheck.check():
281                                                 timer.disable()
282                                                 payload = "%d timer conflict detected, aborting." % (CODE_ERR_LOCAL,)
283                                                 return self.sendLine(payload)
284                                         else:
285                                                 if timersanitycheck.doubleCheck(): timer.disable()
286                                 elif not timer.disabled and disable:
287                                         timer.disable()
288                                 recordTimer.timeChanged(timer)
289                                 sef.sendTimerLine(timer, timerId, last=True)
290                 else:
291                         self.UPDT(' '.join(args))
292
293         def DELT(self, args):
294                 try:
295                         timerId = int(args)
296                 except ValueError:
297                         payload = "%d argument error" % (CODE_SYNTAX,)
298                         return self.sendLine(payload)
299
300                 import NavigationInstance
301                 list = []
302                 recordTimer = NavigationInstance.instance.RecordTimer
303                 list.extend(recordTimer.timer_list)
304                 list.extend(recordTimer.processed_timers)
305                 list.sort(cmp = lambda x, y: x.begin < y.begin)
306
307                 if timerId < 1 or len(list) < timerId:
308                         payload = "%d argument error" % (CODE_SYNTAX,)
309                         return self.sendLine(payload)
310
311                 timer = list[timerId - 1]
312                 recordTimer.removeEntry(timer)
313                 payload = '%d Timer "%d" deleted' % (CODE_OK, timerId)
314                 self.sendLine(payload)
315
316         def MESG(self, data):
317                 if not data:
318                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
319                         return self.sendLine(payload)
320
321                 Notifications.AddNotificationWithID(
322                         NOTIFICATIONID,
323                         MessageBox,
324                         text = data,
325                         type = MessageBox.TYPE_INFO,
326                         timeout = 5,
327                         close_on_any_key = True,
328                 )
329                 payload = "%d Message queued" % (CODE_OK,)
330                 self.sendLine(payload)
331
332         def VOLU(self, args):
333                 volctrl = eDVBVolumecontrol.getInstance()
334                 if args == "mute":
335                         from Components.VolumeControl import VolumeControl
336                         VolumeControl.instance.volMute()
337                 elif args == "+":
338                         from Components.VolumeControl import VolumeControl
339                         VolumeControl.instance.volUp()
340                 elif args == "-":
341                         from Components.VolumeControl import VolumeControl
342                         VolumeControl.instance.volDown()
343                 elif args:
344                         try:
345                                 num = int(args) / 2.55
346                         except ValueError:
347                                 payload = "%d %s" % (CODE_SYNTAX, str(e).replace('\n', ' ').replace('\r', ''))
348                                 return self.sendLine(payload)
349                         else:
350                                 volctr.setVolume(num, num)
351
352                 if volctrl.isMuted():
353                         payload = "%d Audio is mute" % (CODE_OK,)
354                 else:
355                         payload = "%d Audio volume is %d." % (CODE_OK, volctrl.getVolume()*2.55)
356                 self.sendLine(payload)
357
358         def HELP(self, args):
359                 if not len(args) == 2:
360                         payload = "%d data inconsistency error." % (CODE_ERR_LOCAL,)
361                         return self.sendLine(payload)
362                 funcs, args = args
363                 if not args:
364                         funcnames = list(funcs.keys())
365                         funcnames.sort() # make sure this is sorted
366                         payload = "%d-This is Enigma2 VDR-Plugin version %s" % (CODE_HELP, VERSION)
367                         self.sendLine(payload)
368                         payload = "%d-Topics:" % (CODE_HELP,)
369                         x = 5
370                         for func in funcnames:
371                                 if x == 5:
372                                         self.sendLine(payload)
373                                         payload = "%d-    %s" % (CODE_HELP, func)
374                                         x = 1
375                                 else:
376                                         payload +=  "      %s" % (func,)
377                                         x += 1
378                         self.sendLine(payload)
379                         payload = "%d-To report bugs in the implementation send email to" % (CODE_HELP,)
380                         self.sendLine(payload)
381                         payload = "%d-    svdrp AT ritzmo DOT de" % (CODE_HELP,)
382                         self.sendLine(payload)
383                         payload = "%d End of HELP info" % (CODE_HELP,)
384                         self.sendLine(payload)
385                 else:
386                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
387                         return self.sendLine(payload)
388
389         def LSTR(self, args):
390                 if args:
391                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
392                         return self.sendLine(payload)
393
394                 self.movielist.reload()
395
396                 def sendMovieLine(sref, info, begin, counter, last=False):
397                         # <number> <date> <begin> <name>
398                         ctime = info.getInfo(serviceref, iServiceInformation.sTimeCreate) # XXX: difference to begin? just copied this from webif ;-)
399                         datestring = strftime('%d.%m.%y', localtime(ctime))
400                         beginstring = strftime('%H:%M', localtime(ctime))
401                         servicename = ServiceReference(sref).getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')
402                         line = "%d%s%d %s %s %s" % (CODE_OK, '-' if not last else ' ', counter, datestring, beginstring, servicename)
403                         self.sendLine(line)
404
405                 list = self.movielist.list[:]
406                 lastItem = list.pop()
407                 idx = 1
408                 for serviceref, info, begin, unknown in list:
409                         sendMovieLine(serviceref, info, begin, idx)
410                         idx += 1
411                 sendMovieLine(lastItem[0], lastItem[1], lastItem[2], idx, last=True)
412
413         def DELR(self, args):
414                 try:
415                         movieId = int(args)
416                 except ValueError:
417                         payload = "%d argument error" % (CODE_SYNTAX,)
418                         return self.sendLine(payload)
419
420                 sref = self.movielist.list[movieId-1][0]
421                 serviceHandler = eServiceCenter.getInstance()
422                 offline = serviceHandler.offlineOperations(sref)
423
424                 if offline is not None:
425                         if not offline.deleteFromDisk(0):
426                                 payload = '%d Movie "%d" deleted' % (CODE_OK, movieId)
427                                 return self.sendLine(payload)
428
429                 payload = "%d data inconsistency error." % (CODE_ERR_LOCAL,)
430                 self.sendLine(payload)
431
432         def LSTE(self, args):
433                 args = args.split()
434                 first = args and args.pop(0)
435                 #TODO: find out format of "at <time>"
436                 if not first or first in ('now', 'next', 'at') or args and args[1] == "at":
437                         # XXX: "parameter not implemented" might be weird to say in case of no parameters, but who cares :-)
438                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
439                         return self.sendLine(payload)
440                 try:
441                         channelId = int(first)-1
442                         service = self.channelList[channelId]
443                 except ValueError:
444                         # XXX: add support for sref
445                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
446                         return self.sendLine(payload)
447
448                 # handle additional parametes "now" and "next"
449                 type = 0
450                 time = -1
451                 endtime = -1
452                 options = "IBDTSERN"
453                 if args:
454                         options = "IBDTSERNX"
455                         if args[0] == "now":
456                                 type = 0
457                                 endtime = None
458                         elif args[0] == "next":
459                                 type = 1
460                                 endtime = None
461
462                 # fetch data
463                 epgcache = eEPGCache.getInstance()
464                 if endtime is None:
465                         params = (service, type, time)
466                 else:
467                         params = (service, type, time, endtime)
468                 events = epgcache.lookupEvent([options , params])
469
470                 # process data
471                 def sendEventLine(eit, begin, duration, title, description, extended, sref, sname):
472                         payload = "%d-E %d %d %d 0" % (CODE_EPG, eit, begin, duration)
473                         self.sendLine(payload)
474                         payload = "%d-T %s" % (CODE_EPG, title)
475                         self.sendLine(payload)
476                         payload = "%d-S %s" % (CODE_EPG, description)
477                         self.sendLine(payload)
478                         payload = "%d-D %s" % (CODE_EPG, extended.replace('\xc2\x8a', '|'))
479                         self.sendLine(payload)
480                         payload = "%d-e" % (CODE_EPG,)
481                         self.sendLine(payload)
482                 lastItem = events.pop()
483                 payload = "%d-C %s %s" % (CODE_EPG, lastItem[-2], lastItem[-1])
484                 self.sendLine(payload)
485                 for event in events:
486                         sendEventLine(*event)
487                 sendEventLine(*lastItem)
488                 payload = "%d-c" % (CODE_EPG,)
489                 self.sendLine(payload)
490                 payload = "%d End of EPG data" % (CODE_EPG,)
491                 self.sendLine(payload)
492
493         def lineReceived(self, data):
494                 if self.client or not self.transport or not data:
495                         return
496
497                 print("[SVDRP] incoming message:", data)
498                 list = data.split(' ', 1)
499                 command = list.pop(0).upper()
500                 args = list[0] if list else ''
501
502                 # still possible: grab, (partially) hitk, (theoretically) movc, next? (dunno what this does), play, stat and completion of existing commands
503                 funcs = {
504                         'CHAN': self.CHAN,
505                         'DELR': self.DELR,
506                         'DELT': self.DELT,
507                         'HELP': self.HELP,
508                         'LSTC': self.LSTC,
509                         'LSTE': self.LSTE,
510                         'LSTT': self.LSTT,
511                         'LSTR': self.LSTR,
512                         'MESG': self.MESG,
513                         'MODT': self.MODT,
514                         'NEWT': self.NEWT,
515                         'UPDT': self.UPDT,
516                         'QUIT': self.stop,
517                         'VOLU': self.VOLU,
518                 }
519                 if command == "HELP":
520                         args = (funcs, args)
521                 call = funcs.get(command, self.NOT_IMPLEMENTED)
522
523                 try:
524                         call(args)
525                 except Exception as e:
526                         import traceback, sys
527                         traceback.print_exc(file=sys.stdout)
528                         payload = "%d exception occured: %s" % (CODE_ERR, str(e).replace('\n', ' ').replace('\r', ''))
529                         self.sendLine(payload)
530
531 class SimpleVDRProtocolServerFactory(ServerFactory):
532         protocol = SimpleVDRProtocol
533
534         def __init__(self):
535                 self.clients = []
536
537         def addClient(self, client):
538                 self.clients.append(client)
539
540         def removeClient(self, client):
541                 self.clients.remove(client)
542
543         def stopFactory(self):
544                 for client in self.clients:
545                         client.stop()
546
547 class SimpleVDRProtocolAbstraction:
548         serverPort = None
549         pending = 0
550
551         def __init__(self):
552                 self.serverFactory = SimpleVDRProtocolServerFactory()
553                 self.serverPort = reactor.listenTCP(SVDRP_TCP_PORT, self.serverFactory)
554                 self.pending += 1
555
556         def maybeClose(self, resOrFail, defer = None):
557                 self.pending -= 1
558                 if self.pending == 0:
559                         if defer:
560                                 defer.callback(True)
561
562         def stop(self):
563                 defer = Deferred()
564                 if self.serverPort:
565                         d = self.serverPort.stopListening()
566                         if d:
567                                 d.addBoth(self.maybeClose, defer = defer)
568                         else:
569                                 self.pending -= 1
570
571                 if self.pending == 0:
572                         reactor.callLater(1, defer.callback, True)
573                 return defer
574