msmith - in flumotion/branches/platform-3: . flumotion/component/producers/playlist

flumotion-commit at lists.fluendo.com flumotion-commit at lists.fluendo.com
Tue May 22 16:15:12 CEST 2007


Author: msmith
Date: Tue May 22 16:15:10 2007
New Revision: 4994

Added:
   flumotion/branches/platform-3/flumotion/component/producers/playlist/watcher.py
Modified:
   flumotion/branches/platform-3/ChangeLog
   flumotion/branches/platform-3/flumotion/component/producers/playlist/Makefile.am
   flumotion/branches/platform-3/flumotion/component/producers/playlist/playlist.py
   flumotion/branches/platform-3/flumotion/component/producers/playlist/playlist.xml
   flumotion/branches/platform-3/flumotion/component/producers/playlist/playlistparser.py
Log:
        * flumotion/component/producers/playlist/Makefile.am:
        * flumotion/component/producers/playlist/playlist.py:
        * flumotion/component/producers/playlist/playlist.xml:
        * flumotion/component/producers/playlist/playlistparser.py:
        * flumotion/component/producers/playlist/watcher.py:
          Merge current playlist component. Import a private copy of the
          watcher code which doesn't exist in this branch.



Modified: flumotion/branches/platform-3/ChangeLog
==============================================================================
--- flumotion/branches/platform-3/ChangeLog	(original)
+++ flumotion/branches/platform-3/ChangeLog	Tue May 22 16:15:10 2007
@@ -1,3 +1,13 @@
+2007-05-22  Michael Smith <msmith at fluendo.com>
+
+	* flumotion/component/producers/playlist/Makefile.am:
+	* flumotion/component/producers/playlist/playlist.py:
+	* flumotion/component/producers/playlist/playlist.xml:
+	* flumotion/component/producers/playlist/playlistparser.py:
+	* flumotion/component/producers/playlist/watcher.py:
+	  Merge current playlist component. Import a private copy of the
+	  watcher code which doesn't exist in this branch.
+
 2007-05-21  Michael Smith <msmith at fluendo.com>
 
 	* flumotion/component/producers/playlist/design:

Modified: flumotion/branches/platform-3/flumotion/component/producers/playlist/Makefile.am
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/producers/playlist/Makefile.am	(original)
+++ flumotion/branches/platform-3/flumotion/component/producers/playlist/Makefile.am	Tue May 22 16:15:10 2007
@@ -1,7 +1,7 @@
 include $(top_srcdir)/common/python.mk
 
 component_PYTHON = __init__.py playlist.py singledecodebin.py smartscale.py \
-	playlistparser.py
+	playlistparser.py watcher.py
 componentdir = $(libdir)/flumotion/python/flumotion/component/producers/playlist
 component_DATA = playlist.xml
 

Modified: flumotion/branches/platform-3/flumotion/component/producers/playlist/playlist.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/producers/playlist/playlist.py	(original)
+++ flumotion/branches/platform-3/flumotion/component/producers/playlist/playlist.py	Tue May 22 16:15:10 2007
@@ -33,6 +33,7 @@
 import smartscale
 import singledecodebin
 import playlistparser
+import watcher
 
 T_ = messages.gettexter('flumotion')
 
@@ -298,6 +299,8 @@
         props = self.config['properties'];
 
         self._playlistfile = props.get('playlist', None)
+        self._playlistdirectory = props.get('playlist-directory', None)
+        self._baseDirectory = props.get('base-directory', None)
 
         self._width = props.get('width', 320)
         self._height = props.get('height', 240)
@@ -316,6 +319,34 @@
         self.connect_feeders(pipeline)
         return pipeline
 
+    def _watchDirectory(self, dir):
+        self.debug("Watching directory %s", dir)
+        self._filesAdded = {}
+
+        self._directoryWatcher = watcher.DirectoryWatcher(dir)
+        self._directoryWatcher.subscribe(fileChanged=self._watchFileChanged, 
+            fileDeleted=self._watchFileDeleted)
+        self._directoryWatcher.start()
+
+    def _watchFileDeleted(self, file):
+        self.debug("File deleted: %s", file)
+        if file in self._filesAdded:
+            self.playlistparser.playlist.removeItems(file)
+            self._filesAdded.pop(file)
+
+    def _watchFileChanged(self, file):
+        self.debug("File changed: %s", file)
+        if file in self._filesAdded:
+            self.debug("Removing existing items for changed playlist")
+            self.playlistparser.playlist.removeItems(file)
+
+        self._filesAdded[file] = None
+        try:
+            self.debug("Parsing file: %s", file)
+            self.playlistparser.parseFile(file, id=file)
+        except fxml.ParserError, e:
+            self.warning("Failed to parse playlist file: %r", e)
+
     def connect_feeders(self, pipeline):
         # Backport: add this directly here, since it isn't in 
         # feedcomponent010.py
@@ -334,11 +365,17 @@
 
         playlist = playlistparser.Playlist(self)
         self.playlistparser = playlistparser.PlaylistXMLParser(playlist)
-        try:
-            if self._playlistfile:
+        if self._baseDirectory:
+            self.playlistparser.setBaseDirectory(self._baseDirectory)
+
+        if self._playlistfile:
+            try:
                 self.playlistparser.parseFile(self._playlistfile)
-        except fxml.ParserError, e:
-            self.warning("Failed to parse playlist file: %r", e)
+            except fxml.ParserError, e:
+                self.warning("Failed to parse playlist file: %r", e)
+
+        if self._playlistdirectory:
+            self._watchDirectory(self._playlistdirectory)
 
         return defer.succeed(None)
         

Modified: flumotion/branches/platform-3/flumotion/component/producers/playlist/playlist.xml
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/producers/playlist/playlist.xml	(original)
+++ flumotion/branches/platform-3/flumotion/component/producers/playlist/playlist.xml	Tue May 22 16:15:10 2007
@@ -30,8 +30,13 @@
         <property name="channels" type="int"
                   description="Audio channels to output" />
 
-        <property name="playlist" type="string" required="yes"
-                  description="Location of the initial playlist file" />
+        <property name="playlist" type="string"
+                  description="Location of the initial playlist file if any" />
+        <property name="playlist-directory" type="string"
+                  description="Location of a directory to monitor for playlist files to add" />
+
+        <property name="base-directory" type="string"
+                  description="Base directory for relative paths in playlist files" />
       </properties>
     </component>
   </components>
@@ -49,6 +54,7 @@
                 <filename location="singledecodebin.py" />
                 <filename location="playlist.py" />
                 <filename location="playlistparser.py" />
+                <filename location="watcher.py" />
             </directory>
         </directories>
     </bundle>

Modified: flumotion/branches/platform-3/flumotion/component/producers/playlist/playlistparser.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/producers/playlist/playlistparser.py	(original)
+++ flumotion/branches/platform-3/flumotion/component/producers/playlist/playlistparser.py	Tue May 22 16:15:10 2007
@@ -189,6 +189,13 @@
         self._pending_items = []
         self._discovering = False
 
+        self._baseDirectory = None
+
+    def setBaseDirectory(self, baseDir):
+        if not baseDir.endswith('/'):
+            baseDir = baseDir + '/'
+        self._baseDirectory = baseDir
+
     def _discoverPending(self):
         def _discovered(disc, is_media):
             self.debug("Discovered!")
@@ -197,7 +204,11 @@
         def _discoverer_done(disc, is_media):
             if is_media:
                 self.debug("Discovery complete, media found")
-                uri = "file://" + item[0]
+                filename = item[0]
+                if filename[0] != '/' and self._baseDirectory:
+                    filename = self._baseDirectory + filename
+
+                uri = "file://" + filename
                 timestamp = item[1]
                 duration = item[2]
                 offset = item[3]

Added: flumotion/branches/platform-3/flumotion/component/producers/playlist/watcher.py
==============================================================================
--- (empty file)
+++ flumotion/branches/platform-3/flumotion/component/producers/playlist/watcher.py	Tue May 22 16:15:10 2007
@@ -0,0 +1,195 @@
+# -*- Mode: Python -*-
+# vi:si:et:sw=4:sts=4:ts=4
+#
+# Flumotion - a streaming media server
+# Copyright (C) 2006,2007 Fluendo, S.L. (www.fluendo.com).
+# All rights reserved.
+
+# This file may be distributed and/or modified under the terms of
+# the GNU General Public License version 2 as published by
+# the Free Software Foundation.
+# This file is distributed without any warranty; without even the implied
+# warranty of merchantability or fitness for a particular purpose.
+# See "LICENSE.GPL" in the source distribution for more information.
+
+# Licensees having purchased or holding a valid Flumotion Advanced
+# Streaming Server license may use this file in accordance with the
+# Flumotion Advanced Streaming Server Commercial License Agreement.
+# See "LICENSE.Flumotion" in the source distribution for more information.
+
+# Headers in this file shall remain intact.
+
+
+import os
+
+from twisted.internet import reactor
+
+from flumotion.common import log
+
+class BaseWatcher(log.Loggable):
+    """I watch for file changes.
+
+    I am a base class for a file watcher. I can be specialized to watch
+    any set of files.
+    """
+
+    def __init__(self, timeout):
+        """Make a file watcher object.
+        
+        @param timeout: timeout between checks, in seconds
+        @type timeout: int
+        """
+        self.timeout = timeout
+        self._reset()
+        self._subscribeId = 0
+        self.subscribers = {}
+
+    def _reset(self):
+        self._stableData = {}
+        self._changingData = {}
+        self._delayedCall = None
+
+    def _subscribe(self, **events):
+        """Subscribe to events.
+
+        @param kwargs: The events to subscribe to. Subclasses are
+        expected to formalize this dict, specifying which events they
+        support via declaring their kwargs explicitly.
+
+        @returns: A subscription ID that can later be passed to
+        unsubscribe().
+        """
+        sid = self._subscribeId
+        self._subscribeId += 1
+        self.subscribers[sid] = events
+        return sid
+
+    def subscribe(self, fileChanged=None, fileDeleted=None):
+        """Subscribe to events.
+
+        @param fileChanged: A function to call when a file changes. This
+        function will only be called if the file's details (size, mtime)
+        do not change during the timeout period.
+        @type fileChanged: filename -> None
+        @param fileDeleted: A function to call when a file is deleted.
+        @type fileDeleted: filename -> None
+        
+        @returns: A subscription ID that can later be passed to
+        unsubscribe().
+        """
+        return self._subscribe(fileChanged=fileChanged,
+                               fileDeleted=fileDeleted)
+
+    def unsubscribe(self, id):
+        """Unsubscribe from file change notifications.
+
+        @param id: Subscription ID received from subscribe()
+        """
+        del self.subscribers[id]
+
+    def event(self, event, *args, **kwargs):
+        """Fire an event.
+
+        This method is intended for use by object implementations.
+        """
+        for s in self.subscribers.values():
+            if s[event]:
+                s[event](*args, **kwargs)
+
+    def start(self):
+        """Start checking for file changes.
+        
+        Subscribers will be notified asynchronously of changes to the
+        watched files.
+        """
+        def checkFiles():
+            self.log("checking for file changes")
+            new = self.getFileData()
+            changing = self._changingData
+            stable = self._stableData
+            for f in new:
+                if f not in changing:
+                    if f in stable and new[f] == stable[f]:
+                        # no change
+                        pass
+                    else:
+                        self.debug('change start noted for %s', f)
+                        changing[f] = new[f]
+                else:
+                    if new[f] == changing[f]:
+                        self.debug('change finished for %s', f)
+                        del changing[f]
+                        stable[f] = new[f]
+                        self.event('fileChanged', f)
+                    else:
+                        self.log('change continues for %s', f)
+                        changing[f] = new[f]
+            for f in stable:
+                if f not in new:
+                    # deletion
+                    del stable[f]
+                    if f in changing:
+                        del changing[f]
+                    self.debug('file %s has been deleted', f)
+                    self.event('fileDeleted', f)
+            self._delayedCall = reactor.callLater(self.timeout,
+                                                  checkFiles)
+
+        assert self._delayedCall is None
+        checkFiles()
+
+    def stop(self):
+        """Stop checking for file changes.
+        """
+        self._delayedCall.cancel()
+        self._reset()
+
+    def getFileData(self):
+        """
+        @returns: a dict, {filename => DATA}
+        DATA can be anything. In the default implementation it is a pair
+        of (mtime, size).
+        """
+        ret = {}
+        for f in self.getFilesToStat():
+            try:
+                stat = os.stat(f)
+                ret[f] = (stat.st_mtime, stat.st_size)
+            except OSError, e:
+                self.debug('could not read file %f: %s', f,
+                           log.getExceptionMessage(e))
+        return ret
+
+    def getFilesToStat(self):
+        """
+        @returns: sequence of filename
+        """
+        raise NotImplementedError
+
+class DirectoryWatcher(BaseWatcher):
+    """
+    Directory Watcher
+    Watches a directory for new files.
+    """
+    
+    def __init__(self, path, ignorefiles=(), timeout=30):
+        BaseWatcher.__init__(self, timeout)
+        self.path = path
+        self._ignorefiles = ignorefiles
+
+    def getFilesToStat(self):
+        return [os.path.join(self.path, f)
+                for f in os.listdir(self.path)
+                if f not in self._ignorefiles]
+        
+class FilesWatcher(BaseWatcher):
+    """
+    Watches a collection of files for modifications.
+    """
+
+    def __init__(self, files, timeout=30):
+        BaseWatcher.__init__(self, timeout)
+        self._files = files
+
+    def getFilesToStat(self):
+        return self._files


More information about the flumotion-commit mailing list