enigma2 20120714 master -> 20120727 master
[enigma2.git] / usr / lib / enigma2 / python / Plugins / SystemPlugins / MiniDLNASetup / plugin.py
1 from enigma import eEnv, eConsoleAppContainer, eTimer
2
3 from Components.config import config, ConfigYesNo, ConfigEnableDisable, ConfigText, ConfigInteger, ConfigSubList, ConfigSubsection, ConfigSelection, ConfigDirectory, getConfigListEntry
4 from Components.ActionMap import ActionMap
5 from Components.ConfigList import ConfigListScreen
6 from Components.Harddisk import harddiskmanager
7 from Components.Sources.StaticText import StaticText
8 from Components.ResourceManager import resourcemanager
9 from Plugins.Plugin import PluginDescriptor
10 from Screens.MessageBox import MessageBox
11 from Screens.Screen import Screen
12 from Tools.HardwareInfo import HardwareInfo
13
14 from os import path as os_path
15
16 iNetwork = resourcemanager.getResource("iNetwork")
17
18 def onUnMountNotifier(action, mountpoint):
19         if action == harddiskmanager.EVENT_UNMOUNT:
20                 dlna_config.stopDaemon()
21         else:
22                 if config.plugins.minidlna.enabled.value:
23                         dlna_config.startDaemon()
24 harddiskmanager.onUnMount_Notifier.append(onUnMountNotifier)
25
26 """
27 This is a start-stop-script-like python-class
28 """
29 class StartStopDaemon(object):
30         TYPE_START = "start"
31         TYPE_STOP = "stop"
32
33         def __init__(self, daemon, title, bindir=eEnv.resolve('${bindir}'), pidfile=None):
34                 self._daemon = daemon
35                 self._title = title
36
37                 self._path_daemon = '%s/%s' %(bindir, daemon)
38                 self._path_killall = '%s/killall' %(eEnv.resolve('${bindir}'))
39                 self._path_pidfile = pidfile or '/var/run/%s.pid' %(self._daemon)
40
41                 self._console = eConsoleAppContainer()
42                 self._console.appClosed.append(self._consoleCmdFinished)
43
44                 self._cmd_kill = [self._path_killall, self._path_killall, "-HUP", self._daemon]
45                 self._cmd_start = [self._path_daemon, self._path_daemon]
46                 self._restart = False
47                 self.__checkTimer = eTimer()
48                 self.__checkTimer.callback.append(self._checkIfRunning)
49                 self.onCommandFinished = [] # function(TYPE_START/STOP, isSuccess, message)
50
51         def _consoleCmdFinished(self, data):
52                 if self._restart and config.plugins.minidlna.enabled.value:
53                         self._restart = False
54                         self._console.execute(*self._cmd_start)
55                 elif self._restart:
56                         self._restart = False
57                         self._commandFinished(self.TYPE_STOP, True, _("Configuration saved. %s is disabled") %self._title)
58                 else:
59                         self.__checkTimer.start(1500, True)
60
61         def _checkIfRunning(self):
62                 result = self.isRunning()
63                 message = _("%s started successfully with the new configuration") %self._title
64                 if not result:
65                         message = _("%s was NOT started due to an unexpected error") %self._title
66
67                 self._commandFinished(self.TYPE_START, result, message)
68
69         def _commandFinished(self, type, result, message):
70                 for callback in self.onCommandFinished:
71                         callback(type, result, message)
72
73         def _getPid(self):
74                 pid = -1
75                 if os_path.exists(self._path_pidfile):
76                         try:
77                                 with open(self._path_pidfile, "r") as f:
78                                         pid = int(f.readline())
79                         except:
80                                 pass
81                 return pid
82
83         def isRunning(self):
84                 pid = self._getPid()
85                 return os_path.exists("/proc/%s" %pid)
86
87         def stop(self, restart = False):
88                 self._restart = restart
89                 self._console.execute(*self._cmd_kill)
90
91         def start(self):
92                 self._restart = False
93                 if not self.isRunning():
94                         self._console.execute(*self._cmd_start)
95                 else:
96                         self._commandFinished(self.TYPE_START, True, _("%s was already running") %self._title)
97
98         def restart(self):
99                 if self.isRunning():
100                         self.stop(True)
101                 else:
102                         self.start()
103
104 class MiniDLNAConfig:
105         DAEMON = "minidlna"
106         DAEMON_TITLE = "Mediaserver"
107         CONFIG_FILE_PATH = "%s/minidlna.conf" %(eEnv.resolve('${sysconfdir}'))
108
109         MEDIA_TYPE_AUDIO = "A"
110         MEDIA_TYPE_VIDEO = "V"
111         MEDIA_TYPE_PICTURE = "P"
112
113         ROOT_TREE_DEFAULT = "."
114         ROOT_TREE_DIRECTORY = "B"
115         ROOT_TREE_MUSIC = "M"
116         ROOT_TREE_VIDEO = "V"
117         ROOT_TREE_PICTURES = "P"
118
119         BOOL_TEXT = { True : "yes", False : "no" }
120         TEXT_BOOL = { "yes" : True, "no" : False }
121
122         def __init__(self, args = None):
123                 iNetwork = resourcemanager.getResource("iNetwork")
124                 self.adapters = iNetwork.getAdapterList()
125
126                 self.bool_options = ("inotify", "enable_tivo", "strict_dlna")
127                 self.hostname = HardwareInfo().get_device_name()
128                 if len(config.plugins.minidlna.media_dirs) == 0 and config.plugins.minidlna.share_videodirs.value:
129                         for dir in config.movielist.videodirs.value:
130                                 config.plugins.minidlna.media_dirs.append(ConfigText(default=dir, fixed_size = False))
131
132                 self._config = {
133                         "port" : config.plugins.minidlna.port,
134                         "network_interface" : config.plugins.minidlna.network_interface,
135                         "media_dir" : config.plugins.minidlna.media_dirs,
136                         "friendly_name" : config.plugins.minidlna.friendly_name,
137                         "db_dir" : config.plugins.minidlna.db_dir,
138                         "log_dir" : config.plugins.minidlna.log_dir,
139                         "album_art_names" : config.plugins.minidlna.album_art_names,
140                         "inotify" : config.plugins.minidlna.inotify,
141                         "enable_tivo" : config.plugins.minidlna.enable_tivo,
142                         "strict_dlna" : config.plugins.minidlna.strict_dlna,
143                         "serial" : config.plugins.minidlna.serial,
144                         "model_number" : config.plugins.minidlna.model_number,
145                         "root_container" : config.plugins.minidlna.root_container,
146                 }
147
148                 self._init = StartStopDaemon(self.DAEMON, self.DAEMON_TITLE)
149                 self._init.onCommandFinished.append(self._onStartStopCommandFinished)
150                 self.onActionFinished = []
151
152                 harddiskmanager.delayed_device_Notifier.append(self._onDeviceNotifier)
153
154         def get(self):
155                 return self._config
156
157         def _actionFinished(self, result, message):
158                 for callback in self.onActionFinished:
159                         callback(result, message)
160
161         def _onStartStopCommandFinished(self, type, result, message):
162                 self._actionFinished(result, message)
163
164         def _onDeviceNotifier(self, device, action):
165                 self._writeConfig()
166
167         def startDaemon(self):
168                 if harddiskmanager.isMount(config.plugins.minidlna.db_dir.value):
169                         self.apply()
170                 else:
171                         print "[MiniDLNAConfig].startDaemon :: Refusing to start with database in a non-mounted path"
172                         self._actionFinished(False, _("Error! The configured directory is not a mountpoint! Refusing to start!"))
173
174         def stopDaemon(self):
175                 self._init.stop()
176
177         def restartDaemon(self):
178                 self._init.restart()
179
180         def apply(self):
181                 print "[MiniDLNAConfig].apply"
182                 for x in self._config.values():
183                         x.save()
184
185                 if self._writeConfig():
186                         self.restartDaemon()
187                 else:
188                         self._actionFinished(False, _("Error! Couldn't write the configuration file!\nSettings will NOT be lost on exit!"))
189
190         def _writeConfig(self):
191                 lines = []
192                 for key, item in self._config.iteritems():
193                         if key == "media_dir":
194                                 locations = []
195                                 if config.plugins.minidlna.share_videodirs.value:
196                                         locations = config.movielist.videodirs.value or []
197                                 for cfgtxt in item:
198                                         locations.append(cfgtxt.value)
199
200                                 locations = self._unifyLocations( locations )
201                                 config.plugins.minidlna.media_dirs = ConfigSubList()
202                                 for loc in locations:
203                                         lines.append("%s=%s\n" %(key, loc))
204                                         config.plugins.minidlna.media_dirs.append(ConfigText(default=loc, fixed_size = False))
205                                 self._config[key] = config.plugins.minidlna.media_dirs
206                         else:
207                                 value = item.value
208                                 if key in self.bool_options:
209                                         value = self.BOOL_TEXT[value]
210                                 elif key == "db_dir":
211                                         value = "%s/minidlna" %value
212                                 lines.append("%s=%s\n" %(key, value))
213
214                 try:
215                         with open(self.CONFIG_FILE_PATH, "w+") as f:
216                                 f.writelines(lines)
217                         return True
218                 except IOError, e:
219                         print e
220                         return False
221
222         def _unifyLocations(self, locations):
223                 lst = []
224                 for loc in locations:
225                         lst.append( harddiskmanager.getRealPath(loc) )
226                 lst.sort()
227                 lst.reverse()
228                 last = lst[-1]
229                 for i in range(len(lst)-2, -1, -1):
230                         item = lst[i]
231                         if last == item or item.startswith(last):
232                                 del lst[i]
233                         else:
234                                 last = item
235                 return lst
236
237 config.plugins.minidlna = ConfigSubsection()
238 config.plugins.minidlna.enabled = ConfigEnableDisable(default=False)
239 config.plugins.minidlna.share_videodirs = ConfigYesNo(default=True)
240 config.plugins.minidlna.port = ConfigInteger(default=8200, limits = (1, 65535))
241 config.plugins.minidlna.network_interface = ConfigText(default=",".join(iNetwork.getAdapterList()) or "eth0", fixed_size = False)
242 config.plugins.minidlna.media_dirs = ConfigSubList()
243 config.plugins.minidlna.friendly_name = ConfigText(default="%s Mediaserver" %(HardwareInfo().get_device_name()), fixed_size = False)
244 config.plugins.minidlna.db_dir = ConfigDirectory(default="/media/hdd")
245 config.plugins.minidlna.log_dir = ConfigText(default="/tmp/log", fixed_size = False)
246 config.plugins.minidlna.album_art_names = ConfigText("Cover.jpg/cover.jpg/AlbumArtSmall.jpg/albumartsmall.jpg/AlbumArt.jpg/albumart.jpg/Album.jpg/album.jpg/Folder.jpg/folder.jpg/Thumb.jpg/thumb.jpg", fixed_size = False)
247 config.plugins.minidlna.inotify = ConfigYesNo(default=True)
248 config.plugins.minidlna.enable_tivo = ConfigYesNo(default=False)
249 config.plugins.minidlna.strict_dlna = ConfigYesNo(default=False)
250 config.plugins.minidlna.serial = ConfigInteger(default=12345678)
251 config.plugins.minidlna.model_number = ConfigInteger(default=1)
252 config.plugins.minidlna.root_container = ConfigSelection([
253                                 (MiniDLNAConfig.ROOT_TREE_DEFAULT, _("Default")),
254                                 (MiniDLNAConfig.ROOT_TREE_DIRECTORY, _("Directories")),
255                                 (MiniDLNAConfig.ROOT_TREE_MUSIC, _("Music")),
256                                 (MiniDLNAConfig.ROOT_TREE_VIDEO, _("Video")),
257                                 (MiniDLNAConfig.ROOT_TREE_PICTURES, _("Pictures")),
258                         ], default = MiniDLNAConfig.ROOT_TREE_DEFAULT)
259
260 dlna_config = MiniDLNAConfig()
261
262 class MiniDLNASetup(ConfigListScreen, Screen):
263         skin = """
264                 <screen name="MiniDLNASetup" position="center,center" size="560,400" title="Mediaserver (DLNA) Setup">
265                         <ePixmap pixmap="skin_default/buttons/red.png" position="0,0" size="140,40" alphatest="on" />
266                         <ePixmap pixmap="skin_default/buttons/green.png" position="140,0" size="140,40" alphatest="on" />
267                         <ePixmap pixmap="skin_default/buttons/yellow.png" position="280,0" size="140,40" alphatest="on" />
268                         <widget source="key_red" render="Label" position="0,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" backgroundColor="#9f1313" transparent="1" />
269                         <widget source="key_green" render="Label" position="140,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" backgroundColor="#1f771f" transparent="1" />
270                         <widget source="key_yellow" render="Label" position="280,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" backgroundColor="#1f771f" transparent="1" />
271                         <widget name="config" position="5,50" size="550,360" scrollbarMode="showOnDemand" zPosition="1"/>
272                 </screen>"""
273
274         def __init__(self, session, args=0):
275                 Screen.__init__(self, session)
276                 ConfigListScreen.__init__(self, [])
277
278                 self["key_red"] = StaticText(_("Cancel"))
279                 self["key_green"] = StaticText(_("OK"))
280                 self["key_yellow"] = StaticText(_("Shares"))
281                 self["setupActions"] = ActionMap(["SetupActions", "ColorActions"],
282                 {
283                         "red": self.close,
284                         "green": self.save,
285                         "yellow": self._editShares,
286                         "save": self.save,
287                         "cancel": self.close,
288                 }, -2)
289
290                 self.conf = dlna_config.get()
291                 self.createSetup()
292                 self.onLayoutFinish.append(self.layoutFinished)
293                 config.plugins.minidlna.enabled.addNotifier(self._enabledChanged, initial_call = False)
294                 self.onClose.append(self._onClose)
295                 dlna_config.onActionFinished.append( self._actionFinished )
296
297         def _onClose(self):
298                 dlna_config.onActionFinished.remove( self._actionFinished )
299                 config.plugins.minidlna.enabled.removeNotifier(self._enabledChanged)
300                 for x in self["config"].list:
301                         x[1].save()
302
303         def keyLeft(self):
304                 ConfigListScreen.keyLeft(self)
305
306         def keyRight(self):
307                 ConfigListScreen.keyRight(self)
308
309         def createSetup(self):
310                 list = [getConfigListEntry(_("Mediaserver"), config.plugins.minidlna.enabled)]
311                 if config.plugins.minidlna.enabled.value:
312                         list.extend( [
313                                 getConfigListEntry(_("Always share movies"), config.plugins.minidlna.share_videodirs),
314                                 getConfigListEntry(_("Server Name"), config.plugins.minidlna.friendly_name),
315                                 getConfigListEntry(_("Watch Local Folders"), config.plugins.minidlna.inotify),
316                                 getConfigListEntry(_("Enable TIVO Compatibility"), config.plugins.minidlna.enable_tivo),
317                                 getConfigListEntry(_("Strict DLNA Mode"), config.plugins.minidlna.strict_dlna),
318                                 getConfigListEntry(_("Root Container"), config.plugins.minidlna.root_container),
319                         ])
320                         if config.usage.setup_level.index >= 2: # expert
321                                 list.extend([
322                                         getConfigListEntry(_("Port"), config.plugins.minidlna.port),
323                                         getConfigListEntry(_("Base path for Database"), config.plugins.minidlna.db_dir),
324                                         getConfigListEntry(_("Log Directory"), config.plugins.minidlna.log_dir),
325                                         getConfigListEntry(_("Album Art Names"), config.plugins.minidlna.album_art_names),
326                                         getConfigListEntry(_("Serial Number"), config.plugins.minidlna.serial),
327                                         getConfigListEntry(_("Model Number"), config.plugins.minidlna.model_number),
328                                 ])
329
330                 self["config"].list = list
331                 self["config"].l.setList(list)
332
333         def _enabledChanged(self, enabled):
334                 self.createSetup()
335
336         def layoutFinished(self):
337                 self.setTitle(_("Mediaserver (DLNA) Setup"))
338
339         def save(self):
340                 dlna_config.apply()
341
342         def _actionFinished(self, result, text):
343                 if result:
344                         self.session.open(MessageBox, text, type=MessageBox.TYPE_INFO, timeout=3)
345                         self.close()
346                 else:
347                         self.session.open(MessageBox, text, type=MessageBox.TYPE_ERROR, timeout=15)
348
349         def _editShares(self):
350                 self.session.open(MiniDLNAShareSetup)
351
352 class MiniDLNAShareSetup(ConfigListScreen, Screen):
353         skin = """
354                 <screen name="MiniDLNAShareSetup" position="center,center" size="560,400" title="Mediaserver (DLNA) Shares">
355                         <ePixmap pixmap="skin_default/buttons/red.png" position="0,0" size="140,40" alphatest="on" />
356                         <ePixmap pixmap="skin_default/buttons/green.png" position="140,0" size="140,40" alphatest="on" />
357                         <widget source="key_red" render="Label" position="0,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" backgroundColor="#9f1313" transparent="1" />
358                         <widget source="key_green" render="Label" position="140,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" backgroundColor="#1f771f" transparent="1" />
359                         <widget name="config" position="5,50" size="550,360" scrollbarMode="showOnDemand" zPosition="1"/>
360                 </screen>"""
361
362         def __init__(self, session, args=0):
363                 Screen.__init__(self, session)
364                 ConfigListScreen.__init__(self, [])
365
366                 self["key_red"] = StaticText(_("Remove"))
367                 self["key_green"] = StaticText(_("Add"))
368                 self["setupActions"] = ActionMap(["OkCancelActions", "ColorActions"],
369                 {
370                         "red": self._removeDirectory,
371                         "green" : self._addDirectory,
372                         "cancel": self.save,
373                         "ok" : self.save,
374                 }, -2)
375
376                 self._shares = dlna_config.get()['media_dir']
377                 self.createSetup()
378                 self.onLayoutFinish.append(self.layoutFinished)
379
380         def _addDirectory(self):
381                 self._shares.append(ConfigText(default="/media/", fixed_size = False))
382                 self.createSetup()
383
384         def _removeDirectory(self):
385                 index = self["config"].getCurrentIndex()
386                 if index >= 0 and index < len(self._shares):
387                         self._shares.pop(index)
388                         self.createSetup()
389
390         def keyLeft(self):
391                 ConfigListScreen.keyLeft(self)
392
393         def keyRight(self):
394                 ConfigListScreen.keyRight(self)
395
396         def createSetup(self):
397                 list = []
398                 i = 1
399                 for x in self._shares:
400                         list.append(getConfigListEntry(_("Share %s") %(i), x))
401                         i += 1
402                 self["config"].list = list
403                 self["config"].l.setList(list)
404
405         def layoutFinished(self):
406                 self.setTitle(_("Mediaserver (DLNA) Shares"))
407
408         def save(self):
409                 nep = []
410                 hasError = False
411                 for item in self._shares:
412                         if not os_path.exists(item.value):
413                                 nep.append(item.value)
414                                 hasError = True
415                 if hasError:
416                         text = _("""The following configured paths do not exist:\n%s
417                                                 \nProceed anyways?""") %("\n".join(nep))
418                         self.session.openWithCallback(self._onSaveErrorDecision, MessageBox, text, type=MessageBox.TYPE_YESNO)
419                 else:
420                         self._save()
421
422         def _save(self):
423                 self._shares.save()
424                 self.close()
425
426         def _onSaveErrorDecision(self, decision):
427                 if decision:
428                         self._save()
429
430 def main(session, **kwargs):
431         session.open(MiniDLNASetup)
432
433 def autostart(reason, **kwargs):
434         if reason == 0 and config.plugins.minidlna.enabled.value:
435                 dlna_config.startDaemon()
436
437 def menu(menuid, **kwargs):
438         if menuid == "system":
439                 return [(_("Mediaserver (DLNA)"), main, "media_server_setup", None)]
440         else:
441                 return []
442
443 def Plugins(**kwargs):
444         return [PluginDescriptor(name=_("Mediaserver (DLNA) Setup"), description=_("Setup the DLNA Mediaserver"), where = PluginDescriptor.WHERE_MENU, needsRestart = True, fnc=menu),
445                         PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc=autostart)
446                 ]