SeriesPlugin 2.4.3: Changed handling of special char encoding
[enigma2-plugins.git] / seriesplugin / src / SeriesPlugin.py
1 # -*- coding: utf-8 -*-
2 # by betonme @2012
3
4 import re
5
6 import os, sys, traceback
7
8 from time import localtime, strftime
9 from datetime import datetime
10
11 # Localization
12 from . import _
13
14 from datetime import datetime
15
16 from Components.config import config
17
18 from enigma import eServiceReference, iServiceInformation, eServiceCenter, ePythonMessagePump
19 from ServiceReference import ServiceReference
20
21 # Plugin framework
22 from Modules import Modules
23
24 # Tools
25 from Tools.BoundFunction import boundFunction
26 from Tools.Directories import resolveFilename, SCOPE_PLUGINS
27 from Tools.Notifications import AddPopup
28 from Screens.MessageBox import MessageBox
29
30 # Plugin internal
31 from IdentifierBase import IdentifierBase
32 from Logger import splog
33 from Channels import ChannelsBase
34 from ThreadQueue import ThreadQueue
35 from threading import Thread, currentThread, _get_ident
36 #from enigma import ePythonMessagePump
37
38
39 try:
40         if(config.plugins.autotimer.timeout.value == 1):
41                 config.plugins.autotimer.timeout.value = 5
42                 config.plugins.autotimer.save()
43 except Exception as e:
44         pass
45
46
47 # Constants
48 AUTOTIMER_PATH  = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/AutoTimer/" )
49 SERIESPLUGIN_PATH  = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/SeriesPlugin/" )
50
51
52 # Globals
53 instance = None
54
55 CompiledRegexpNonDecimal = re.compile(r'[^\d]+')
56
57 def dump(obj):
58         for attr in dir(obj):
59                 splog( "SP: %s = %s" % (attr, getattr(obj, attr)) )
60
61
62 def getInstance():
63         global instance
64         
65         if instance is None:
66                 
67                 from plugin import VERSION
68                 
69                 splog("SP: SERIESPLUGIN NEW INSTANCE " + VERSION)
70                 
71                 try:
72                         from Tools.HardwareInfo import HardwareInfo
73                         splog( "SP: DeviceName " + HardwareInfo().get_device_name().strip() )
74                         #from os import uname
75                         #uname()[0]'Linux'
76                         #uname()[1]'dm7080'
77                         #uname()[2]'3.4-3.0-dm7080'
78                         #uname()[3]'#13 SMP Thu Dec 4 00:25:51 UTC 2014'
79                         #uname()[4]'mips'
80                 except:
81                         sys.exc_clear()
82                 
83                 try:
84                         from Components.About import about
85                         splog( "SP: EnigmaVersion " + about.getEnigmaVersionString().strip() )
86                         splog( "SP: ImageVersion " + about.getVersionString().strip() )
87                 except:
88                         sys.exc_clear()
89                 
90                 try:
91                         #http://stackoverflow.com/questions/1904394/python-selecting-to-read-the-first-line-only
92                         splog( "SP: dreamboxmodel " + open("/proc/stb/info/model").readline().strip() )
93                         splog( "SP: imageversion " + open("/etc/image-version").readline().strip() )
94                         splog( "SP: imageissue " + open("/etc/issue.net").readline().strip() )
95                 except:
96                         sys.exc_clear()
97                 
98                 try:
99                         for key, value in config.plugins.seriesplugin.dict().iteritems():
100                                 splog( "SP: config..%s = %s" % (key, str(value.value)) )
101                 except Exception as e:
102                         sys.exc_clear()
103                 
104                 #try:
105                 #       if os.path.exists(SERIESPLUGIN_PATH):
106                 #               dirList = os.listdir(SERIESPLUGIN_PATH)
107                 #               for fname in dirList:
108                 #                       splog( "SP: ", fname, datetime.fromtimestamp( int( os.path.getctime( os.path.join(SERIESPLUGIN_PATH,fname) ) ) ).strftime('%Y-%m-%d %H:%M:%S') )
109                 #except Exception as e:
110                 #       sys.exc_clear()
111                 #try:
112                 #       if os.path.exists(AUTOTIMER_PATH):
113                 #               dirList = os.listdir(AUTOTIMER_PATH)
114                 #               for fname in dirList:
115                 #                       splog( "SP: ", fname, datetime.fromtimestamp( int( os.path.getctime( os.path.join(AUTOTIMER_PATH,fname) ) ) ).strftime('%Y-%m-%d %H:%M:%S') )
116                 #except Exception as e:
117                 #       sys.exc_clear()
118                 
119                 instance = SeriesPlugin()
120                 #instance[os.getpid()] = SeriesPlugin()
121                 splog( "SP: ", strftime("%a, %d %b %Y %H:%M:%S", localtime()) )
122         
123         return instance
124
125 def resetInstance():
126         if config.plugins.seriesplugin.lookup_counter.isChanged():
127                 config.plugins.seriesplugin.lookup_counter.save()
128         
129         global instance
130         if instance is not None:
131                 splog("SP: SERIESPLUGIN INSTANCE STOP")
132                 instance.stop()
133                 instance = None
134         
135         from Cacher import cache
136         global cache
137         cache = {}
138
139
140 def refactorTitle(org, data):
141         if data:
142                 season, episode, title, series = data
143                 if config.plugins.seriesplugin.pattern_title.value and not config.plugins.seriesplugin.pattern_title.value == "Off":
144                         if config.plugins.seriesplugin.replace_chars.value:
145                                 repl = re.compile('['+config.plugins.seriesplugin.replace_chars.value.replace("\\", "\\\\\\\\")+']')
146                                 splog("SP: refactor org1", org)
147                                 org = repl.sub('', org)
148                                 splog("SP: refactor org2", org)
149                         #return config.plugins.seriesplugin.pattern_title.value.strip().format( **{'org': org, 'season': season, 'episode': episode, 'title': title, 'series': series} )
150                         cust_title = config.plugins.seriesplugin.pattern_title.value.strip().format( **{'org': org, 'season': season, 'episode': episode, 'title': title, 'series': series} )
151                         cust_title.replace('&amp;','&').replace('&apos;',"'").replace('&gt;','>').replace('&lt;','<').replace('&quot;','"').replace('/',' ').replace('  ',' ')
152                         splog("SP: refactor org3", cust_title)
153                         return cust_title
154                 else:
155                         return org
156         else:
157                 return org
158
159 def refactorDescription(org, data):
160         if data:
161                 season, episode, title, series = data
162                 if config.plugins.seriesplugin.pattern_description.value and not config.plugins.seriesplugin.pattern_description.value == "Off":
163                         if config.plugins.seriesplugin.replace_chars.value:
164                                 repl = re.compile('['+config.plugins.seriesplugin.replace_chars.value.replace("\\", "\\\\\\\\")+']')
165                                 splog("SP: refactor des1", org)
166                                 org = repl.sub('', org)
167                                 splog("SP: refactor des2", org)
168                         ##if season == 0 and episode == 0:
169                         ##      description = config.plugins.seriesplugin.pattern_description.value.strip().format( **{'org': org, 'title': title, 'series': series} )
170                         ##else:
171                         #description = config.plugins.seriesplugin.pattern_description.value.strip().format( **{'org': org, 'season': season, 'episode': episode, 'title': title, 'series': series} )
172                         #description = description.replace("\n", " ")
173                         #return description
174                         cust_plot = config.plugins.seriesplugin.pattern_description.value.strip().format( **{'org': org, 'season': season, 'episode': episode, 'title': title, 'series': series} )
175                         cust_plot = cust_plot.replace("\n", " ").replace('&amp;','&').replace('&apos;',"'").replace('&gt;','>').replace('&lt;','<').replace('&quot;','"').replace('/',' ').replace('  ',' ')
176                         splog("SP: refactor des3", cust_plot)
177                         return cust_plot
178                 else:
179                         return org
180         else:
181                 return org
182
183
184 class ThreadItem:
185         def __init__(self, identifier = None, callback = None, name = None, begin = None, end = None, service = None):
186                 self.identifier = identifier
187                 self.callback = callback
188                 self.name = name
189                 self.begin = begin
190                 self.end = end
191                 self.service = service
192
193
194 class SeriesPluginWorker(Thread):
195         
196         def __init__(self, callback):
197                 Thread.__init__(self)
198                 self.callback = callback
199                 self.__running = False
200                 self.__messages = ThreadQueue()
201                 self.__pump = ePythonMessagePump()
202                 try:
203                         self.__pump_recv_msg_conn = self.__pump.recv_msg.connect(self.gotThreadMsg)
204                 except:
205                         self.__pump.recv_msg.get().append(self.gotThreadMsg)
206                 self.__queue = ThreadQueue()
207
208         def empty(self):
209                 return self.__queue.empty()
210         
211         def finished(self):
212                 return not self.__running
213
214         def add(self, item):
215                 
216                 from ctypes import CDLL
217                 SYS_gettid = 4222
218                 libc = CDLL("libc.so.6")
219                 tid = libc.syscall(SYS_gettid)
220                 splog('SP: Worker add from thread: ', currentThread(), _get_ident(), self.ident, os.getpid(), tid )
221                 
222                 self.__queue.push(item)
223                 
224                 if not self.__running:
225                         self.__running = True
226                         self.start() # Start blocking code in Thread
227         
228         def gotThreadMsg(self, msg=None):
229                 
230                 from ctypes import CDLL
231                 SYS_gettid = 4222
232                 libc = CDLL("libc.so.6")
233                 tid = libc.syscall(SYS_gettid)
234                 splog('SP: Worker got message: ', currentThread(), _get_ident(), self.ident, os.getpid(), tid )
235                 
236                 data = self.__messages.pop()
237                 if callable(self.callback):
238                         self.callback(data)
239
240         def stop(self):
241                 self.running = False
242                 try:
243                         self.__pump.recv_msg.get().remove(self.gotThreadMsg)
244                 except:
245                         pass
246                 self.__pump_recv_msg_conn = None
247         
248         def run(self):
249                 
250                 from ctypes import CDLL
251                 SYS_gettid = 4222
252                 libc = CDLL("libc.so.6")
253                 tid = libc.syscall(SYS_gettid)
254                 splog('SP: Worker got message: ', currentThread(), _get_ident(), self.ident, os.getpid(), tid )
255                 
256                 while not self.__queue.empty():
257                         
258                         # NOTE: we have to check this here and not using the while to prevent the parser to be started on shutdown
259                         if not self.__running: break
260                         
261                         item = self.__queue.pop()
262                         
263                         splog('SP: Worker is processing')
264                         
265                         result = None
266                         
267                         try:
268                                 result = item.identifier.getEpisode(
269                                         item.name, item.begin, item.end, item.service
270                                 )
271                         except Exception, e:
272                                 splog("SP: Worker: Exception:", str(e))
273                                 
274                                 # Exception finish job with error
275                                 result = str(e)
276                         
277                         config.plugins.seriesplugin.lookup_counter.value += 1
278                         
279                         if result and len(result) == 4:
280                                 splog("SP: Worker: result callback")
281                                 season, episode, title, series = result
282                                 season = int(CompiledRegexpNonDecimal.sub('', season))
283                                 episode = int(CompiledRegexpNonDecimal.sub('', episode))
284                                 title = title.strip()
285                                 if config.plugins.seriesplugin.replace_chars.value:
286                                         repl = re.compile('['+config.plugins.seriesplugin.replace_chars.value.replace("\\", "\\\\\\\\")+']')
287                                         
288                                         splog("SP: refactor title", title)
289                                         title = repl.sub('', title)
290                                         splog("SP: refactor title", title)
291                                         
292                                         splog("SP: refactor series", series)
293                                         series = repl.sub('', series)
294                                         splog("SP: refactor series", series)
295                                 self.__messages.push( (item.callback, (season, episode, title, series)) )
296                         else:
297                                 splog("SP: Worker: result failed")
298                                 self.__messages.push( (item.callback, result) )
299                         self.__pump.send(0)
300                         #from twisted.internet import reactor
301                         #reactor.callFromThread(self.gotThreadMsg)
302                 
303                 splog('SP: Worker: list is emty, done')
304                 Thread.__init__(self)
305                 self.__running = False
306
307
308 class SeriesPlugin(Modules, ChannelsBase):
309
310         def __init__(self):
311                 splog("SP: Main: Init")
312                 self.thread = SeriesPluginWorker(self.gotResult)
313                 Modules.__init__(self)
314                 ChannelsBase.__init__(self)
315                 
316                 self.serviceHandler = eServiceCenter.getInstance()
317                 
318                 #http://bugs.python.org/issue7980
319                 datetime.strptime('2012-01-01', '%Y-%m-%d')
320                 
321                 self.identifier_elapsed = self.instantiateModuleWithName( config.plugins.seriesplugin.identifier_elapsed.value )
322                 #splog(self.identifier_elapsed)
323                 
324                 self.identifier_today = self.instantiateModuleWithName( config.plugins.seriesplugin.identifier_today.value )
325                 #splog(self.identifier_today)
326                 
327                 self.identifier_future = self.instantiateModuleWithName( config.plugins.seriesplugin.identifier_future.value )
328                 #splog(self.identifier_future)
329                 
330                 pattern = config.plugins.seriesplugin.pattern_title.value
331                 pattern = pattern.replace("{org:s}", "(.+)")
332                 pattern = re.sub('{season:?\d*d?}', '\d+', pattern)
333                 pattern = re.sub('{episode:?\d*d?}', '\d+', pattern)
334                 pattern = pattern.replace("{title:s}", ".+")
335                 self.compiledRegexpSeries = re.compile(pattern)
336         
337         ################################################
338         # Identifier functions
339         def getIdentifier(self, future=False, today=False, elapsed=False):
340                 if elapsed:
341                         return self.identifier_elapsed and self.identifier_elapsed.getName()
342                 elif today:
343                         return self.identifier_today and self.identifier_today.getName()
344                 elif future:
345                         return self.identifier_future and self.identifier_future.getName()
346                 else:
347                         return None
348         
349         def getEpisode(self, callback, name, begin, end=None, service=None, future=False, today=False, elapsed=False, rename=False):
350                 #available = False
351                 
352                 if config.plugins.seriesplugin.skip_during_records.value:
353                         try:
354                                 import NavigationInstance
355                                 if NavigationInstance.instance.RecordTimer.isRecording():
356                                         splog("SP: Main: Skip check during running records")
357                                         return
358                         except:
359                                 pass
360                 
361                 # Check for episode information in title
362                 match = self.compiledRegexpSeries.match(name)
363                 if match:
364                         #splog(match.group(0))     # Entire match
365                         #splog(match.group(1))     # First parenthesized subgroup
366                         if not rename and config.plugins.seriesplugin.skip_pattern_match.value:
367                                 splog("SP: Main: Skip check because of pattern match")
368                                 return
369                         if match.group(1):
370                                 name = match.group(1)
371                 
372                 begin = datetime.fromtimestamp(begin)
373                 splog("SP: Main: begin:", begin.strftime('%Y-%m-%d %H:%M:%S'))
374                 end = datetime.fromtimestamp(end)
375                 splog("SP: Main: end:", end.strftime('%Y-%m-%d %H:%M:%S'))
376                 
377                 if elapsed:
378                         identifier = self.identifier_elapsed
379                 elif today:
380                         identifier = self.identifier_today
381                 elif future:
382                         identifier = self.identifier_future
383                 else:
384                         identifier = None
385                 
386                 if not identifier:
387                         callback( "Error: No identifier available" )
388                 
389                 elif identifier.channelsEmpty():
390                         callback( "Error: Open setup and channel editor" )
391                 
392                 else:
393                         # Reset title search depth on every new request
394                         identifier.search_depth = 0;
395                         
396                         # Reset the knownids on every new request
397                         identifier.knownids = []
398                         
399                         #if isinstance(service, eServiceReference):
400                         try:
401                                 serviceref = service.toString()
402                         #else:
403                         except:
404                                 sys.exc_clear()
405                                 serviceref = str(service)
406                         serviceref = re.sub('::.*', ':', serviceref)
407
408                         self.thread.add( ThreadItem(identifier, callback, name, begin, end, serviceref) )
409                         
410                         return identifier.getName()
411
412         def getEpisodeBlocking(self, name, begin, end=None, service=None, future=False, today=False, elapsed=False, rename=False):
413                 #available = False
414                 
415                 if config.plugins.seriesplugin.skip_during_records.value:
416                         try:
417                                 import NavigationInstance
418                                 if NavigationInstance.instance.RecordTimer.isRecording():
419                                         splog("SP: Main: Skip check during running records")
420                                         return
421                         except:
422                                 pass
423                 
424                 # Check for episode information in title
425                 match = self.compiledRegexpSeries.match(name)
426                 if match:
427                         #splog(match.group(0))     # Entire match
428                         #splog(match.group(1))     # First parenthesized subgroup
429                         if not rename and config.plugins.seriesplugin.skip_pattern_match.value:
430                                 splog("SP: Main: Skip check because of pattern match")
431                                 return
432                         if match.group(1):
433                                 name = match.group(1)
434                 
435                 begin = datetime.fromtimestamp(begin)
436                 splog("SP: Main: begin:", begin.strftime('%Y-%m-%d %H:%M:%S'))
437                 end = datetime.fromtimestamp(end)
438                 splog("SP: Main: end:", end.strftime('%Y-%m-%d %H:%M:%S'))
439                 
440                 if elapsed:
441                         identifier = self.identifier_elapsed
442                 elif today:
443                         identifier = self.identifier_today
444                 elif future:
445                         identifier = self.identifier_future
446                 else:
447                         identifier = None
448                 
449                 if not identifier:
450                         callback( "Error: No identifier available" )
451                 
452                 elif identifier.channelsEmpty():
453                         callback( "Error: Open setup and channel editor" )
454                 
455                 else:
456                         # Reset title search depth on every new request
457                         identifier.search_depth = 0;
458                         
459                         # Reset the knownids on every new request
460                         identifier.knownids = []
461                         
462                         #if isinstance(service, eServiceReference):
463                         try:
464                                 serviceref = service.toString()
465                         #else:
466                         except:
467                                 sys.exc_clear()
468                                 serviceref = str(service)
469                         serviceref = re.sub('::.*', ':', serviceref)
470                         
471                         result = None
472                         
473                         try:
474                                 result = identifier.getEpisode( name, begin, end, serviceref )
475                         except Exception, e:
476                                 splog("SP: Worker: Exception:", str(e))
477                                 
478                                 # Exception finish job with error
479                                 result = str(e)
480                         
481                         config.plugins.seriesplugin.lookup_counter.value += 1
482                         
483                         splog("SP: Worker: result")
484                         if result and len(result) == 4:
485                                 season, episode, title, series = result
486                                 season = int(CompiledRegexpNonDecimal.sub('', season))
487                                 episode = int(CompiledRegexpNonDecimal.sub('', episode))
488                                 title = title.strip()
489                                 splog("SP: Worker: result callback")
490                                 return (season, episode, title, series)
491                         else:
492                                 splog("SP: Worker: result failed")
493                                 return result
494
495         def gotResult(self, msg):
496                 splog("SP: Main: Thread: gotResult:", msg)
497                 callback, data = msg
498                 if callable(callback):
499                         callback(data)
500                 
501                 if (config.plugins.seriesplugin.lookup_counter.value == 10) \
502                         or (config.plugins.seriesplugin.lookup_counter.value == 100) \
503                         or (config.plugins.seriesplugin.lookup_counter.value % 1000 == 0):
504                         from plugin import ABOUT
505                         about = ABOUT.format( **{'lookups': config.plugins.seriesplugin.lookup_counter.value} )
506                         AddPopup(
507                                 about,
508                                 MessageBox.TYPE_INFO,
509                                 -1,
510                                 'SP_PopUp_ID_About'
511                         )
512
513         def stop(self):
514                 splog("SP: Main: stop")
515                 self.thread.stop()
516                 # NOTE: while we don't need to join the thread, we should do so in case it's currently parsing
517                 #self.thread.join()
518                 
519                 self.thread = None
520                 self.saveXML()