SeriesPlugin 1.0: First public version
[enigma2-plugins.git] / seriesplugin / src / Channels.py
1 #######################################################################
2 #
3 #    Series Plugin for Enigma-2
4 #    Coded by betonme (c) 2012 <glaserfrank(at)gmail.com>
5 #    Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD
6 #
7 #    This program is free software; you can redistribute it and/or
8 #    modify it under the terms of the GNU General Public License
9 #    as published by the Free Software Foundation; either version 2
10 #    of the License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU General Public License for more details.
16 #
17 #######################################################################
18
19 import os
20 import re
21
22 # Config
23 from Components.config import config
24
25 #from enigma import eEPGCache, eServiceReference, eServiceCenter
26 from ServiceReference import ServiceReference
27
28 from Screens.MessageBox import MessageBox
29 from Tools.BoundFunction import boundFunction
30
31 # XML
32 from xml.etree.cElementTree import ElementTree, tostring, parse, Element, SubElement, Comment
33 from Tools.XMLTools import stringToXML
34
35 # Plugin internal
36 from . import _
37 from Logger import splog
38
39 try:
40         #Python >= 2.7
41         from collections import OrderedDict
42 except:
43         from OrderedDict import OrderedDict
44
45
46 CompiledRegexpSeries = re.compile('(.*)[ _][Ss]{,1}\d{1,2}[EeXx]\d{1,2}.*')  #Only for S01E01 OR 01x01 + optional title
47 def removeEpisodeInfo(text):
48         # Very basic Series Episode remove function
49         m = CompiledRegexpSeries.match(text)
50         if m:
51                 #splog(m.group(0))     # Entire match
52                 #splog(m.group(1))     # First parenthesized subgroup
53                 if m.group(1):
54                         text = m.group(1)
55         return text
56
57
58 ChannelReplaceDict = OrderedDict([
59         ('\(S\)', ''),
60         ('HD', ''),
61         ('III', 'drei'),
62         ('II',  'zwei'),
63         #('I',   'eins'),
64         ('ARD', 'DasErste'),
65         ('\+', 'Plus'),
66         ('0', 'null'),
67         ('1', 'eins'),
68         ('2', 'zwei'),
69         ('3', 'drei'),
70         ('4', 'vier'),
71         ('5', 'fuenf'),
72         ('6', 'sechs'),
73         ('7', 'sieben'),
74         ('8', 'acht'),
75         ('9', 'neun'),
76         ('\xc3\xa4', 'ae'),
77         ('\xc3\xb6', 'oe'),
78         ('\xc3\xbc', 'ue'),
79         ('\xc3\x84', 'ae'),
80         ('\xc3\x96', 'oe'),
81         ('\xc3\x9c', 'ue'),
82         ('\xc3\x9f', 'ss'),
83 ])
84 CompiledRegexpChannelUnify = re.compile('|'.join(ChannelReplaceDict))
85 CompiledRegexpChannelRemoveSpecialChars = re.compile('[^a-zA-Z0-9]')
86 def unifyChannel(text):
87         def translate(match):
88                 m = match.group(0)
89                 return ChannelReplaceDict.get(m, m)
90         
91         text = CompiledRegexpChannelUnify.sub(translate, text)
92         text = text.decode("utf-8").encode("latin1")
93         text = CompiledRegexpChannelRemoveSpecialChars.sub('', text)
94         return text.strip().lower()
95
96
97 channels = {}  # channels[reference] = ( name, [ (name1, uname1), (name2, uname2), ... ] )
98 channels_changed = False
99         
100 def lookupServiceAlternatives(service):
101         global channels, channels_changed
102         
103         splog("lookupServiceAlternatives service", service)
104         ref = str(service)
105         ref = re.sub('::.*', ':', ref)
106         splog("lookupServiceAlternatives ref", ref)
107         #splog("lookupServiceAlternatives channels before", channels)
108         splog("lookupServiceAlternatives ref in channels", ref in channels)
109         if ref in channels:
110                 name, alternatives = channels.get(ref)
111         else:
112                 name = ServiceReference(ref).getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')
113                 alternatives = [ ( name, unifyChannel(name) ), ]
114                 channels[ref] = ( name, alternatives )
115                 channels_changed = True
116         
117         splog("lookupServiceAlternatives channels")
118         for channel in channels:
119                 splog(channel)
120         splog("lookupServiceAlternatives alternatives")
121         for alternative in alternatives:
122                 splog(alternative)
123         
124         return alternatives
125
126 def compareChannels(locals, remote, service):
127         #global channels
128         
129         uremote = unifyChannel(remote)          
130         splog(locals, remote, uremote, len(uremote))
131         
132         for name, uname in locals:
133                 if uname == uremote:
134                         # The channels are equal
135                         return True
136                 elif uname in uremote or uremote in uname:
137                         # Parts of the channels are equal
138                         return True
139                 #elif uname == "":
140                 #       # The local channel is empty
141                 #       return True
142                 elif "unknown" in uname:
143                         # The local channel is unknown
144                         return True
145 #TBD Because of E2 Update 05.2013
146 #       else:
147 #               # Ask the user only if we are called from SeriesPluginInfoScreen
148 #               from SeriesPluginInfoScreen import instance
149 #               if config.plugins.seriesplugin.channel_popups.value and instance:
150 #                       names = [ name for name, uname in locals ]
151 #                       instance.session.openWithCallback(
152 #                               boundFunction(channelEqual, locals, remote, uremote, service),
153 #                               MessageBox,
154 #                               "SeriesPlugin:\nAre these channels equal?\n"+str(remote)+"\n"+str(names),
155 #                               type = MessageBox.TYPE_YESNO
156 #                       )
157         return False
158
159 def channelEqual(locals, remote, uremote, service, equal):
160         global channels, channels_changed
161         
162         if equal:
163                 # Add remote to alternative channels
164                 for reference, namealternatives in channels.iteritems():
165                         name, alternatives = namealternatives
166                         splog("locals, alternatives:", locals, alternatives)
167                         if alternatives == locals:
168                                 ref = str(service)
169                                 ref = re.sub('::.*', ':', ref)
170                                 name = ServiceReference(ref).getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')
171                                 
172                                 # Maybe we should append the new entry
173                                 channels[ref] = ( name, [(remote, uremote)] )
174                                 channels_changed = True
175                                 splog("SP channel added ", channels)
176                                 break
177                 # The channel will be saved, restart the last request
178                 #TODO show message ?!?
179
180
181 class ChannelsFile(object):
182
183         def __init__(self):
184                 self.mtime = -1
185                 self.cache = ""
186
187         def readXML(self):
188                 path = config.plugins.seriesplugin.channel_file.value
189                 
190                 # Abort if no config found
191                 if not os.path.exists(path):
192                         splog("No configuration file present")
193                         return None
194                 
195                 # Parse if mtime differs from whats saved
196                 mtime = os.path.getmtime(path)
197                 if mtime == self.mtime:
198                         # No changes in configuration, won't read again
199                         return self.cache
200                 
201                 # Parse XML
202                 try:
203                         etree = parse(path).getroot()
204                 except Exception as e:
205                         splog("Exception in readXML: " + str(e))
206                         etree = None
207                         mtime = -1
208                 
209                 # Save time and cache file content
210                 self.mtime = mtime
211                 self.cache = etree
212                 return self.cache
213
214         def writeXML(self, etree):
215                 path = config.plugins.seriesplugin.channel_file.value
216                 
217                 def indent(elem, level=0):
218                         i = "\n" + level*"  "
219                         if len(elem):
220                                 if not elem.text or not elem.text.strip():
221                                         elem.text = i + "  "
222                                 if not elem.tail or not elem.tail.strip():
223                                         elem.tail = i
224                                 for elem in elem:
225                                         indent(elem, level+1)
226                                 if not elem.tail or not elem.tail.strip():
227                                         elem.tail = i
228                         else:
229                                 if level and (not elem.tail or not elem.tail.strip()):
230                                         elem.tail = i
231                 
232                 indent(etree)
233                 data = tostring(etree, 'utf-8')
234                 
235                 f = None
236                 try:
237                         f = open(path, 'w')
238                         if data:
239                                 f.writelines(data)
240                 except Exception as e:
241                         splog("Exception in writeXML: " + str(e))
242                 finally:
243                         if f is not None:
244                                 f.close()
245                 
246                 # Save time and cache file content
247                 self.mtime = os.path.getmtime( path )
248                 self.cache = etree
249
250
251 class ChannelsBase(ChannelsFile):
252
253         def __init__(self):
254                 ChannelsFile.__init__(self)
255                 
256                 global channels, channels_changed
257                 channels = {}  # channels[reference] = ( name, [ (name1, uname1), (name2, uname2), ... ] )
258                 channels_changed = False
259                 
260                 self.loadXML()
261         
262         def loadXML(self):
263                 global channels
264                 
265                 # Read xml config file
266                 root = self.readXML()
267                 if root:
268                         channels = {}
269                         
270                         # Parse Config
271                         def parse(root):
272                                 channels = {}
273                                 if root:
274                                         for element in root.findall("Channel"):
275                                                 name = element.get("name", "")
276                                                 reference = element.get("reference", "")
277                                                 if name and reference:
278                                                         #alternatives = []
279                                                         alternatives = [(name, unifyChannel(name))]
280                                                         for alternative in element.findall("Alternative"):
281                                                                 alternatives.append( ( alternative.text , unifyChannel(alternative.text) ) )
282                                                         channels[reference] = (name, list(set(alternatives)))
283                                 return channels
284                         
285                         channels = parse( root )
286                         splog("loadXML channels", channels)
287                 else:
288                         channels = {}
289
290         def saveXML(self):
291                 global channels, channels_changed
292                 
293                 if channels_changed:
294                         
295                         # Generate List in RAM
296                         root = None
297                         splog("saveXML channels", channels)
298                         
299                         # Build Header
300                         from plugin import NAME, VERSION
301                         root = Element(NAME)
302                         root.set('version', VERSION)
303                         root.append(Comment(_("Don't edit this manually unless you really know what you are doing")))
304                         
305                         # Build Body
306                         def build(root, channels):
307                                 if channels:
308                                         for reference, namealternatives in channels.iteritems():
309                                                 name, alternatives = namealternatives
310                                                 # Add channel
311                                                 element = SubElement( root, "Channel", name = stringToXML(name), reference = stringToXML(reference) )
312                                                 # Add alternatives
313                                                 if alternatives:
314                                                         for name, uname in alternatives:
315                                                                 #SubElement( element, "Alternative", identifier = stringToXML(NOTUSED) ).text = stringToXML(name)
316                                                                 SubElement( element, "Alternative" ).text = stringToXML(name)
317                                 return root
318                         
319                         root = build( root, channels )
320                         
321                         self.writeXML( root )