IMDb: keep search string after manual search
[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 getTimerList(self):
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(key=attrgetter('begin'))
171                 return list
172
173         def LSTT(self, args):
174                 list = self.getTimerList()
175                 lastItem = list.pop()
176                 idx = 1
177                 for timer in list:
178                         self.sendTimerLine(timer, idx)
179                         idx += 1
180                 if not self.sendTimerLine(lastItem, idx, last=True):
181                         # send error if last item failed to send, else the other end might get stuck
182                         payload = "%d data inconsistency error." % (CODE_ERR_LOCAL,)
183                         self.sendLine(payload)
184
185         def UPDT(self, args):
186                 # <id> <settings>
187                 args = args.split(None, 1)
188                 if len(args) != 2:
189                         payload = "%d argument error" % (CODE_SYNTAX,)
190                         return self.sendLine(payload)
191
192                 try:
193                         timerId = int(args[0])
194                 except ValueError:
195                         payload = "%d argument error" % (CODE_SYNTAX,)
196                         return self.sendLine(payload)
197
198                 list = self.getTimerList()
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 as e:
218                         payload = "%d argument error" % (CODE_SYNTAX,)
219                         return self.sendLine(payload)
220                 except KeyError as 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                         list = self.getTimerList()
259
260                         if timerId < 1 or len(list) < timerId:
261                                 payload = "%d argument error" % (CODE_SYNTAX,)
262                                 return self.sendLine(payload)
263
264                         timer = list[timerId - 1]
265                         disable = args[1] == 'off'
266                         if disable and timer.isRunning():
267                                 payload = "%d timer is running, not disabling." % (CODE_ERR_LOCAL,)
268                                 return self.sendLine(payload)
269                         else:
270                                 if timer.disabled and not disable:
271                                         timer.enable()
272                                         tsc = TimerSanityCheck(recordTimer.timer_list, timer)
273                                         if not timersanitycheck.check():
274                                                 timer.disable()
275                                                 payload = "%d timer conflict detected, aborting." % (CODE_ERR_LOCAL,)
276                                                 return self.sendLine(payload)
277                                         else:
278                                                 if timersanitycheck.doubleCheck(): timer.disable()
279                                 elif not timer.disabled and disable:
280                                         timer.disable()
281                                 recordTimer.timeChanged(timer)
282                                 sef.sendTimerLine(timer, timerId, last=True)
283                 else:
284                         self.UPDT(' '.join(args))
285
286         def DELT(self, args):
287                 try:
288                         timerId = int(args)
289                 except ValueError:
290                         payload = "%d argument error" % (CODE_SYNTAX,)
291                         return self.sendLine(payload)
292
293                 list = self.getTimerList()
294
295                 if timerId < 1 or len(list) < timerId:
296                         payload = "%d argument error" % (CODE_SYNTAX,)
297                         return self.sendLine(payload)
298
299                 timer = list[timerId - 1]
300                 recordTimer.removeEntry(timer)
301                 payload = '%d Timer "%d" deleted' % (CODE_OK, timerId)
302                 self.sendLine(payload)
303
304         def MESG(self, data):
305                 if not data:
306                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
307                         return self.sendLine(payload)
308
309                 Notifications.AddNotificationWithID(
310                         NOTIFICATIONID,
311                         MessageBox,
312                         text = data,
313                         type = MessageBox.TYPE_INFO,
314                         timeout = 5,
315                         close_on_any_key = True,
316                 )
317                 payload = "%d Message queued" % (CODE_OK,)
318                 self.sendLine(payload)
319
320         def VOLU(self, args):
321                 volctrl = eDVBVolumecontrol.getInstance()
322                 if args == "mute":
323                         from Components.VolumeControl import VolumeControl
324                         VolumeControl.instance.volMute()
325                 elif args == "+":
326                         from Components.VolumeControl import VolumeControl
327                         VolumeControl.instance.volUp()
328                 elif args == "-":
329                         from Components.VolumeControl import VolumeControl
330                         VolumeControl.instance.volDown()
331                 elif args:
332                         try:
333                                 num = int(args) / 2.55
334                         except ValueError:
335                                 payload = "%d %s" % (CODE_SYNTAX, str(e).replace('\n', ' ').replace('\r', ''))
336                                 return self.sendLine(payload)
337                         else:
338                                 volctr.setVolume(num, num)
339
340                 if volctrl.isMuted():
341                         payload = "%d Audio is mute" % (CODE_OK,)
342                 else:
343                         payload = "%d Audio volume is %d." % (CODE_OK, volctrl.getVolume()*2.55)
344                 self.sendLine(payload)
345
346         def HELP(self, args):
347                 if not len(args) == 2:
348                         payload = "%d data inconsistency error." % (CODE_ERR_LOCAL,)
349                         return self.sendLine(payload)
350                 funcs, args = args
351                 if not args:
352                         funcnames = sorted(funcs.keys())
353                         payload = "%d-This is Enigma2 VDR-Plugin version %s" % (CODE_HELP, VERSION)
354                         self.sendLine(payload)
355                         payload = "%d-Topics:" % (CODE_HELP,)
356                         x = 5
357                         for func in funcnames:
358                                 if x == 5:
359                                         self.sendLine(payload)
360                                         payload = "%d-    %s" % (CODE_HELP, func)
361                                         x = 1
362                                 else:
363                                         payload +=  "      %s" % (func,)
364                                         x += 1
365                         self.sendLine(payload)
366                         payload = "%d-To report bugs in the implementation send email to" % (CODE_HELP,)
367                         self.sendLine(payload)
368                         payload = "%d-    svdrp AT ritzmo DOT de" % (CODE_HELP,)
369                         self.sendLine(payload)
370                         payload = "%d End of HELP info" % (CODE_HELP,)
371                         self.sendLine(payload)
372                 else:
373                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
374                         return self.sendLine(payload)
375
376         def LSTR(self, args):
377                 if args:
378                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
379                         return self.sendLine(payload)
380
381                 self.movielist.reload()
382
383                 def sendMovieLine(sref, info, begin, counter, last=False):
384                         # <number> <date> <begin> <name>
385                         ctime = info.getInfo(serviceref, iServiceInformation.sTimeCreate) # XXX: difference to begin? just copied this from webif ;-)
386                         datestring = strftime('%d.%m.%y', localtime(ctime))
387                         beginstring = strftime('%H:%M', localtime(ctime))
388                         servicename = ServiceReference(sref).getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')
389                         line = "%d%s%d %s %s %s" % (CODE_OK, '-' if not last else ' ', counter, datestring, beginstring, servicename)
390                         self.sendLine(line)
391
392                 list = self.movielist.list[:]
393                 lastItem = list.pop()
394                 idx = 1
395                 for serviceref, info, begin, unknown in list:
396                         sendMovieLine(serviceref, info, begin, idx)
397                         idx += 1
398                 sendMovieLine(lastItem[0], lastItem[1], lastItem[2], idx, last=True)
399
400         def DELR(self, args):
401                 try:
402                         movieId = int(args)
403                 except ValueError:
404                         payload = "%d argument error" % (CODE_SYNTAX,)
405                         return self.sendLine(payload)
406
407                 sref = self.movielist.list[movieId-1][0]
408                 serviceHandler = eServiceCenter.getInstance()
409                 offline = serviceHandler.offlineOperations(sref)
410
411                 if offline is not None:
412                         if not offline.deleteFromDisk(0):
413                                 payload = '%d Movie "%d" deleted' % (CODE_OK, movieId)
414                                 return self.sendLine(payload)
415
416                 payload = "%d data inconsistency error." % (CODE_ERR_LOCAL,)
417                 self.sendLine(payload)
418
419         def LSTE(self, args):
420                 args = args.split()
421                 first = args and args.pop(0)
422                 #TODO: find out format of "at <time>"
423                 if not first or first in ('now', 'next', 'at') or args and args[1] == "at":
424                         # XXX: "parameter not implemented" might be weird to say in case of no parameters, but who cares :-)
425                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
426                         return self.sendLine(payload)
427                 try:
428                         channelId = int(first)-1
429                         service = self.channelList[channelId]
430                 except ValueError:
431                         # XXX: add support for sref
432                         payload = "%d parameter not implemented" % (CODE_IMP_PARAM,)
433                         return self.sendLine(payload)
434
435                 # handle additional parametes "now" and "next"
436                 type = 0
437                 time = -1
438                 endtime = -1
439                 options = "IBDTSERN"
440                 if args:
441                         options = "IBDTSERNX"
442                         if args[0] == "now":
443                                 type = 0
444                                 endtime = None
445                         elif args[0] == "next":
446                                 type = 1
447                                 endtime = None
448
449                 # fetch data
450                 epgcache = eEPGCache.getInstance()
451                 if endtime is None:
452                         params = (service, type, time)
453                 else:
454                         params = (service, type, time, endtime)
455                 events = epgcache.lookupEvent([options , params])
456                 if not events:
457                         return self.sendLine("550 No schedule found")
458
459                 # process data
460                 def sendEventLine(eit, begin, duration, title, description, extended, sref, sname):
461                         payload = "%d-E %d %d %d 0" % (CODE_EPG, eit, begin, duration)
462                         self.sendLine(payload)
463                         payload = "%d-T %s" % (CODE_EPG, title)
464                         self.sendLine(payload)
465                         payload = "%d-S %s" % (CODE_EPG, description)
466                         self.sendLine(payload)
467                         payload = "%d-D %s" % (CODE_EPG, extended.replace('\xc2\x8a', '|'))
468                         self.sendLine(payload)
469                         payload = "%d-e" % (CODE_EPG,)
470                         self.sendLine(payload)
471                 lastItem = events.pop()
472                 payload = "%d-C %s %s" % (CODE_EPG, lastItem[-2], lastItem[-1])
473                 self.sendLine(payload)
474                 for event in events:
475                         sendEventLine(*event)
476                 sendEventLine(*lastItem)
477                 payload = "%d-c" % (CODE_EPG,)
478                 self.sendLine(payload)
479                 payload = "%d End of EPG data" % (CODE_EPG,)
480                 self.sendLine(payload)
481
482         def lineReceived(self, data):
483                 if self.client or not self.transport or not data:
484                         return
485
486                 print("[SVDRP] incoming message:", data)
487                 list = data.split(' ', 1)
488                 command = list.pop(0).upper()
489                 args = list[0] if list else ''
490
491                 # still possible: grab, (partially) hitk, (theoretically) movc, next? (dunno what this does), play, stat and completion of existing commands
492                 funcs = {
493                         'CHAN': self.CHAN,
494                         'DELR': self.DELR,
495                         'DELT': self.DELT,
496                         'HELP': self.HELP,
497                         'LSTC': self.LSTC,
498                         'LSTE': self.LSTE,
499                         'LSTT': self.LSTT,
500                         'LSTR': self.LSTR,
501                         'MESG': self.MESG,
502                         'MODT': self.MODT,
503                         'NEWT': self.NEWT,
504                         'UPDT': self.UPDT,
505                         'QUIT': self.stop,
506                         'VOLU': self.VOLU,
507                 }
508                 if command == "HELP":
509                         args = (funcs, args)
510                 call = funcs.get(command, self.NOT_IMPLEMENTED)
511
512                 try:
513                         call(args)
514                 except Exception as e:
515                         import traceback, sys
516                         traceback.print_exc(file=sys.stdout)
517                         payload = "%d exception occured: %s" % (CODE_ERR, str(e).replace('\n', ' ').replace('\r', ''))
518                         self.sendLine(payload)
519
520 class SimpleVDRProtocolServerFactory(ServerFactory):
521         protocol = SimpleVDRProtocol
522
523         def __init__(self):
524                 self.clients = []
525
526         def addClient(self, client):
527                 self.clients.append(client)
528
529         def removeClient(self, client):
530                 self.clients.remove(client)
531
532         def stopFactory(self):
533                 for client in self.clients:
534                         client.stop()
535
536 class SimpleVDRProtocolAbstraction:
537         serverPort = None
538         pending = 0
539
540         def __init__(self):
541                 self.serverFactory = SimpleVDRProtocolServerFactory()
542                 self.serverPort = reactor.listenTCP(SVDRP_TCP_PORT, self.serverFactory)
543                 self.pending += 1
544
545         def maybeClose(self, resOrFail, defer = None):
546                 self.pending -= 1
547                 if self.pending == 0:
548                         if defer:
549                                 defer.callback(True)
550
551         def stop(self):
552                 defer = Deferred()
553                 if self.serverPort:
554                         d = self.serverPort.stopListening()
555                         if d:
556                                 d.addBoth(self.maybeClose, defer = defer)
557                         else:
558                                 self.pending -= 1
559
560                 if self.pending == 0:
561                         reactor.callLater(1, defer.callback, True)
562                 return defer
563