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