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