[pitivi] A bit more code, still not 100% done with the design and docs.
- From: Edward Hervey <edwardrv src gnome org>
- To: svn-commits-list gnome org
- Subject: [pitivi] A bit more code, still not 100% done with the design and docs.
- Date: Fri, 17 Apr 2009 09:35:24 -0400 (EDT)
commit a85aeb2503582a9ed2f476c98fa9b288c644e7a5
Author: Edward Hervey <bilboed bilboed com>
Date: Tue Mar 17 22:52:13 2009 +0100
A bit more code, still not 100% done with the design and docs.
---
pitivi/formatters/base.py | 242 ++++++++++++++++++++++++++++++++++++++++---
pitivi/formatters/format.py | 35 ++++---
pitivi/project.py | 212 ++++++++++++++------------------------
pitivi/sourcelist.py | 5 +
pitivi/utils.py | 30 ++++++
5 files changed, 359 insertions(+), 165 deletions(-)
diff --git a/pitivi/formatters/base.py b/pitivi/formatters/base.py
index a3c1992..3ae0fa0 100644
--- a/pitivi/formatters/base.py
+++ b/pitivi/formatters/base.py
@@ -24,68 +24,277 @@ Base Formatter classes
"""
from pitivi.project import Project
+from pitivi.utils import uri_is_reachable, uri_is_valid
+from pitivi.signalinterface import Signallable
class FormatterError(Exception):
pass
+class FormatterURIError(Exception):
+ """An error occured with a URI"""
+
class FormatterLoadError(FormatterError):
- pass
+ """An error occured while loading the Project"""
+
+class FormatterParseError(FormatterLoadError):
+ """An error occured while parsing the project file"""
class FormatterSaveError(FormatterError):
- pass
+ """An error occured while saving the Project"""
+
+class FormatterOverwriteError(FormatterSaveError):
+ """A project can't be saved because it will be overwritten"""
# FIXME : How do we handle interaction with the UI ??
# Do we blindly load everything and let the UI figure out what's missing from
# the loaded project ?
-class Formatter(object):
+class Formatter(object, Signallable):
"""
Provides convenience methods for storing and loading
Project files.
+ Signals:
+ - C{missing-uri} : A uri can't be found.
+
@cvar description: Description of the format.
@type description: C{str}
+ @cvar project: The project being loaded/saved
+ @type project: L{Project}
"""
+ __signals__ = {
+ "missing-uri" : ["uri"]
+ }
+
description = "Description of the format"
+ def __init__(self):
+ # mapping of directory changes
+ # key : old path
+ # value : new path
+ self.directorymapping = {}
+
+ self.project = None
+
+ #{ Load/Save methods
+
def loadProject(self, location):
"""
Loads the project from the given location.
- @type location: L{str}
- @param location: The location of a file. Needs to be an absolute URI.
+ @postcondition: There is no guarantee that the returned project
+ is fully loaded. Callers should check
- @rtype: C{Project}
- @return: The C{Project}
+ @type location: C{URI}
+ @param location: The location of a file. Needs to be an absolute URI.
+ @rtype: L{Project}
+ @return: The L{Project}
@raise FormatterLoadError: If the file couldn't be properly loaded.
"""
- raise FormatterLoadError("No Loading feature")
+ # check if the location is
+ # .. a uri
+ # .. a valid uri
+ # .. a reachable valid uri
+ # FIXME : Allow subclasses to handle this for 'online' (non-file://) URI
+ if not uri_is_valid(location) or not uri_is_reachable(location):
+ raise FormatterURIError()
- def saveProject(self, project, location):
+ # parse the format (subclasses)
+ # FIXME : maybe have a convenience method for opening a location
+ self.parse(location)
+
+ # create a NewProject
+ # FIXME : allow subclasses to create their own Project subclass
+ project = Project()
+
+ # ask for all sources being used
+ uris = []
+ factories = []
+ wtf = []
+ for x in self._getSources():
+ if isinstance(x, SourceFactory):
+ factories.append(x)
+ elif isinstance(x, str):
+ uris.append(x)
+ else:
+ raise FormatterLoadError("Got invalid sources !")
+
+ # from this point on we're safe !
+ self.project = project
+ project._formatter = self
+
+ # add all factories to the project sourcelist
+ for fact in factories:
+ project.sources.addFactory(fact)
+
+ # if all sources were discovered, or don't require discovering,
+ if uris == []:
+ # then
+ # .. Fill in the timeline
+ self._fillTimeline(self)
+ # .. make the project as loaded
+ self.project.loaded = True
+ else:
+ # else
+ # .. connect to the sourcelist 'ready' signal
+ self.project.sources.connect("ready", self._sourcesReadyCb)
+ # .. Add all uris to be discovered to the project sourcelist
+ self.project.loaded = False
+ self.project.sources.addUris(uris)
+
+ # finally return the project.
+ return self.project
+
+ def saveProject(self, project, location, overwrite=False):
"""
Saves the given project to the given location.
- @type project: C{Project}
+ @type project: L{Project}
@param project: The Project to store.
- @type location: L{str}
+ @type location: C{URI}
@param location: The location where to store the project. Needs to be
an absolute URI.
+ @param overwrite: Whether to overwrite existing location.
+ @type overwrite: C{bool}
+ @raise FormatterURIError: If the location isn't a valid C{URI}.
+ @raise FormatterOverwriteError: If the location already exists and overwrite is False.
@raise FormatterSaveError: If the file couldn't be properly stored.
"""
- raise FormatterSaveError("No Saving feature")
+ if not uri_is_valid(location):
+ raise FormatterURIError()
+ if overwrite == False and uri_is_reachable(location):
+ raise FormatterOverwriteError()
+ return self._saveProject(project, location)
- def canHandle(self, location):
+ #}
+
+ @classmethod
+ def canHandle(cls, location):
"""
Can this Formatter load the project at the given location.
- @type location: L{str}
+ @type location: C{URI}
@param location: The location. Needs to be an absolute C{URI}.
- @rtype: L{bool}
- @return: True if this Formatter can load the C{Project}.
+ @rtype: C{bool}
+ @return: True if this Formatter can load the L{Project}.
"""
raise NotImplementedError
+ #{ Subclass methods
+
+ def _saveProject(self, project, location):
+ """
+ Save the given project to the given location.
+
+ Sub classes should implement this.
+
+ @precondition: The location is guaranteed to be writable.
+
+ @param project: the project to store.
+ @type project: L{Project}
+ @type location: C{URI}
+ @param location: The location where to store the project. Needs to be
+ an absolute URI.
+ """
+ raise NotImplementedError
+
+ def _getSources(self):
+ """
+ Return all the sources used in a project.
+
+ To be implemented by subclasses.
+
+ The returned sources can be either:
+ - C{URI}
+ - any L{SourceFactory} fully-discovered subclass.
+
+ The returned locations (C{URI}) must be valid uri. Subclasses can
+ call L{validateSourceURI} to make sure the C{URI} is valid.
+
+ @precondition: L{_parse} will be called before, so subclasses can
+ use any information they extracted during that call.
+ @returns: A list of sources used in the given project.
+ """
+ raise NotImplementedError
+
+ def _parse(self, location):
+ """
+ Open and parse the given location.
+
+ To be implemented by subclasses.
+
+ If any error occurs during this step, subclasses should raise the
+ FormatterParseError exception.
+ """
+ raise NotImplementedError
+
+ #{ Missing uri methods
+
+ def addMapping(self, oldpath, newpath):
+ """
+ Add a mapping for moved files.
+
+ This should be called in callbacks from 'missing-uri'.
+
+ @param oldpath: Old location (as provided by 'missing-uri').
+ @type oldpath: C{URI}
+ @param newpath: The new location corresponding to oldpath.
+ @type newpath: C{URI}
+ """
+ raise NotImplementedError
+
+ def validateSourceURI(self, uri):
+ """
+ Makes sure the given uri is accessible for reading.
+
+ Subclasses should call this method for any C{URI} they parse,
+ in order to make sure they have the valid C{URI} on this given
+ setup.
+
+ @returns: The valid 'uri'. It might be different from the
+ input. Sub-classes must use this for any URI they wish to
+ read from. If no valid 'uri' can be found, None will be
+ returned.
+ @rtype: C{URI} or C{None}
+ """
+ if not uri_is_valid(uri):
+ return None
+
+ # skip non local uri
+ if not uri.split('://', 1)[0] in ["file"]:
+ return uri
+
+ # first check the good old way
+ if not uri_is_valid(uri) or not uri_is_reachable(uri):
+ return None
+
+ localpath = uri.split('://', 1)[1]
+
+ # else let's figure out if we have a compatible mapping
+ for k, v in self.directorymapping.iteritems():
+ if localpath.startswith(k):
+ return localpath.replace(k, v, 1)
+
+ # else, let's fire the signal...
+ self.emit('missing-uri', uri)
+
+ # and check again
+ for k, v in self.directorymapping.iteritems():
+ if localpath.startswith(k):
+ return localpath.replace(k, v, 1)
+
+ # Houston, we have lost contact with mission://fail
+ return None
+
+ #}
+
+ def _sourcesReadyCb(self, sources):
+ self._fillTimeline(self)
+ self.project.loaded = True
+ Project.emit(self.project, 'loaded')
+
+
class LoadOnlyFormatter(Formatter):
def saveProject(self, project, location):
raise FormatterSaveError("No Saving feature")
@@ -101,3 +310,4 @@ class DefaultFormatter(Formatter):
description = "PiTiVi default file format"
pass
+
diff --git a/pitivi/formatters/format.py b/pitivi/formatters/format.py
index 1930182..53d9d7a 100644
--- a/pitivi/formatters/format.py
+++ b/pitivi/formatters/format.py
@@ -25,7 +25,7 @@ High-level tools for using Formatters
# FIXME : We need a registry of all available formatters
-def load_project(uri, formatter=None):
+def load_project(uri, formatter=None, missinguricallback=None):
"""
Load the project from the given location.
@@ -34,31 +34,40 @@ def load_project(uri, formatter=None):
@type uri: L{str}
@param uri: The location of the project. Needs to be an
absolute URI.
- @type formatter: C{Formatter}
+ @type formatter: L{Formatter}
@param formatter: If specified, try loading the project with that
- C{Formatter}. If not specified, will try all available C{Formatter}s.
+ L{Formatter}. If not specified, will try all available L{Formatter}s.
@raise FormatterLoadError: If the location couldn't be properly loaded.
- @return: The loaded C{Project}
+ @param missinguricallback: A callback that will be used if some
+ files to load can't be found anymore. The callback shall call the
+ formatter's addMapping() method with the moved location.
+ @type missinguricallback: C{callable}
+ @return: The project. The caller needs to ensure the loading is
+ finished before using it. See the 'loaded' property and signal of
+ L{Project}.
+ @rtype: L{Project}.
"""
raise NotImplementedError
-def save_project(project, uri, formatter=None):
+def save_project(project, uri, formatter=None, overwrite=False):
"""
- Save the C{Project} to the given location.
+ Save the L{Project} to the given location.
If specified, use the given formatter.
- @type project: C{Project}
- @param project: The C{Project} to save.
+ @type project: L{Project}
+ @param project: The L{Project} to save.
@type uri: L{str}
@param uri: The location to store the project to. Needs to
be an absolute URI.
- @type formatter: C{Formatter}
- @param formatter: The C{Formatter} to use to store the project if specified.
+ @type formatter: L{Formatter}
+ @param formatter: The L{Formatter} to use to store the project if specified.
If it is not specified, then it will be saved at its original format.
+ @param overwrite: Whether to overwrite existing location.
+ @type overwrite: C{bool}
@raise FormatterSaveError: If the file couldn't be properly stored.
- @return: Whether the file was successfully stored
- @rtype: L{bool}
+
+ @see: L{Formatter.saveProject}
"""
raise NotImplementedError
@@ -69,7 +78,7 @@ def can_handle_location(uri):
@type uri: L{str}
@param uri: The location of the project. Needs to be an
absolute URI.
- @return: Whether the location contains a valid C{Project}.
+ @return: Whether the location contains a valid L{Project}.
@rtype: L{bool}
"""
raise NotImplementedError
diff --git a/pitivi/project.py b/pitivi/project.py
index 040aa7f..c55e4a8 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -40,41 +40,45 @@ from pitivi.configure import APPNAME
from pitivi.signalinterface import Signallable
from pitivi.action import ViewAction
+class ProjectError(Exception):
+ """Project error"""
+ pass
+
+class ProjectSaveLoadError(ProjectError):
+ """Error while loading/saving project"""
+ pass
+
class Project(object, Signallable, Loggable):
- """ The base class for PiTiVi projects
- Signals
-
- boolean save-uri-requested()
- The the current project has been requested to save itself, but
- needs a URI to which to save. Handlers should first call
- setUri(), with the uri to save the file (optionally
- specifying the file format) and return True, or simply
- return False to cancel the file save operation.
-
- boolean confirm-overwrite()
- The project has been requested to save itself, but the file on
- disk either already exists, or has been changed since the previous
- load/ save operation. In this case, the project wants permition to
- overwrite before continuing. handlers should return True if it is
- ok to overwrit the file, or False otherwise. By default, this
- signal handler assumes True.
-
- void settings-changed()
- The project settings have changed
-
- @cvar timeline: The timeline
+ """The base class for PiTiVi projects
+
+ @ivar name: The name of the project
+ @type name: C{str}
+ @ivar description: A description of the project
+ @type description: C{str}
+ @ivar sources: The sources used by this project
+ @type sources: L{SourceList}
+ @ivar timeline: The timeline
@type timeline: L{Timeline}
- @cvar pipeline: The timeline's pipeline
+ @ivar pipeline: The timeline's pipeline
@type pipeline: L{Pipeline}
- @cvar factory: The timeline factory
+ @ivar factory: The timeline factory
@type factory: L{TimelineSourceFactory}
+ @ivar format: The format under which the project is currently stored.
+ @type format: L{FormatterClass}
+ @ivar loaded: Whether the project is fully loaded or not.
+ @type loaded: C{bool}
+
+ Signals:
+ - C{missing-plugins} : A plugin is missing for the given uri
+ - C{loaded} : The project is now fully loaded.
"""
__signals__ = {
"save-uri-requested" : None,
"confirm-overwrite" : ["location"],
"settings-changed" : None,
- "missing-plugins": ["uri", "detail", "description"]
+ "missing-plugins": ["uri", "detail", "description"],
+ "loaded" : None
}
def __init__(self, name="", uri=None, **kwargs):
@@ -91,9 +95,13 @@ class Project(object, Signallable, Loggable):
self.urichanged = False
self.format = None
self.sources = SourceList(self)
+
self.settingssigid = 0
self._dirty = False
+ # formatter instance used for loading project.
+ self._formatter = None
+
self.sources.connect('missing-plugins', self._sourceListMissingPluginsCb)
self.timeline = Timeline()
@@ -111,110 +119,15 @@ class Project(object, Signallable, Loggable):
self.view_action = ViewAction()
self.view_action.addProducers(self.factory)
- # don't want to make calling load() necessary for blank projects
- if self.uri == None:
- self._loaded = True
- else:
- self._loaded = False
+ # the loading formatter will set this accordingly
+ self.loaded = True
def release(self):
self.pipeline.release()
self.pipeline = None
- def load(self):
- """ call this to load a project from a file (once) """
- if self._loaded:
- # should this return false?
- self.warning("Already loaded !!!")
- return True
- try:
- res = self._load()
- except:
- self.error("An Exception was raised during loading !")
- traceback.print_exc()
- res = False
- finally:
- return res
-
- def _load(self):
- """
- loads the project from a file
- Private method, use load() instead
- """
- self.log("uri:%s", self.uri)
- self.debug("Creating timeline")
- # FIXME : This should be discovered !
- saveformat = "pickle"
- if self.uri and file_is_project(self.uri):
- loader = ProjectSaver.newProjectSaver(saveformat)
- path = gst.uri_get_location(self.uri)
- fileobj = open(path, "r")
- try:
- tree = loader.openFromFile(fileobj)
- self.fromDataFormat(tree)
- except ProjectLoadError:
- self.error("Error while loading the project !!!")
- return False
- finally:
- fileobj.close()
- self.format = saveformat
- self.urichanged = False
- self.debug("Done loading !")
- return True
- return False
-
- def _save(self):
- """ internal save function """
- if uri_is_valid(self.uri):
- path = gst.uri_get_location(self.uri)
- else:
- self.warning("uri '%s' is invalid, aborting save", self.uri)
- return False
-
- #TODO: a bit more sophisticated overwite detection
- if os.path.exists(path) and self.urichanged:
- overwriteres = self.emit("confirm-overwrite", self.uri)
- if overwriteres == False:
- self.log("aborting save because overwrite was denied")
- return False
-
- try:
- fileobj = open(path, "w")
- loader = ProjectSaver.newProjectSaver(self.format)
- tree = self.toDataFormat()
- loader.saveToFile(tree, fileobj)
- self._dirty = False
- self.urichanged = False
- self.log("Project file saved successfully !")
- return True
- except IOError:
- return False
-
- def save(self):
- """ Saves the project to the project's current file """
- self.log("saving...")
- if self.uri:
- return self._save()
-
- self.log("requesting for a uri to save to...")
- saveres = self.emit("save-uri-requested")
- if saveres == None or saveres == True:
- self.log("'save-uri-requested' returned True, self.uri:%s", self.uri)
- if self.uri:
- return self._save()
-
- self.log("'save-uri-requested' returned False or uri wasn't set, aborting save")
- return False
-
- def saveAs(self):
- """ Saves the project to the given file name """
- if not self.emit("save-uri-requested"):
- return False
- if not self.uri:
- return False
- return self._save()
-
- # setting methods
+ #{ Settings methods
+
def _settingsChangedCb(self, unused_settings):
self.emit('settings-changed')
@@ -289,22 +202,49 @@ class Project(object, Signallable, Loggable):
return settings
+ #}
+
+ def _sourceListMissingPluginsCb(self, source_list, uri, detail, description):
+ return self.emit('missing-plugins', uri, detail, description)
+
+ #{ Save and Load features
+
+ def save(self, location=None, overwrite=False):
+ """
+ Save the project to the given location.
+
+ @param location: The location to write to. If not specified, the
+ current project location will be used (if set).
+ @type location: C{URI}
+ @param overwrite: Whether to overwrite existing location.
+ @type overwrite: C{bool}
+
+ @raises ProjectSaveLoadError: If no uri was provided and none was set
+ previously.
+ """
+ self.log("saving...")
+ location = location or self.uri
+
+ if location == None:
+ raise ProjectSaveLoadError("Location unknown")
+
+ save_project(self, location or self.uri, self.format,
+ overwrite)
+
+ self.uri = location
+
def setModificationState(self, state):
self._dirty = state
def hasUnsavedModifications(self):
return self._dirty
- def _sourceListMissingPluginsCb(self, source_list, uri, detail, description):
- return self.emit('missing-plugins', uri, detail, description)
-
-def uri_is_valid(uri):
- """ Checks if the given uri is a valid uri (of type file://) """
- return gst.uri_get_protocol(uri) == "file"
+ def markLoaded(self):
+ """
+ Mark the project as loaded.
-def file_is_project(uri):
- """ returns True if the given uri is a PitiviProject file"""
- if not uri_is_valid(uri):
- raise NotImplementedError(
- _("%s doesn't yet handle non local projects") % APPNAME)
- return os.path.isfile(gst.uri_get_location(uri))
+ Will emit the 'loaded' signal. Only meant to be used by
+ L{Formatter}s.
+ """
+ self.loaded = True
+ self.emit('loaded')
diff --git a/pitivi/sourcelist.py b/pitivi/sourcelist.py
index 468f96a..f7cac59 100644
--- a/pitivi/sourcelist.py
+++ b/pitivi/sourcelist.py
@@ -31,6 +31,11 @@ class SourceList(object, Signallable, Loggable):
"""
Contains the sources for a project, stored as FileSourceFactory
+ @ivar project: The owner project
+ @type project: L{Project}
+ @ivar discoverer: The discoverer used
+ @type discoverer: L{Discoverer}
+
Signals:
- C{file_added} : A file has been completely discovered and is valid.
- C{file_removed} : A file was removed from the SourceList.
diff --git a/pitivi/utils.py b/pitivi/utils.py
index ba5dc75..e521b89 100644
--- a/pitivi/utils.py
+++ b/pitivi/utils.py
@@ -24,6 +24,7 @@
import gobject
import gst, bisect
+import os
from pitivi.signalinterface import Signallable
import pitivi.log.log as log
@@ -191,6 +192,35 @@ def filter_(caps):
f.props.caps = gst.caps_from_string(caps)
return f
+
+## URI functions
+
+
+def uri_is_valid(uri):
+ """Checks if the given uri is a valid uri (of type file://)
+
+ @param uri: The location to check
+ @type uri: C{URI}
+ """
+ res = gst.uri_is_valid(uri) and gst.uri_get_protocol(uri) == "file"
+ if res:
+ return len(os.path.basename(gst.uri_get_location(uri))) > 0
+ return res
+
+def uri_is_reachable(uri):
+ """ Check whether the given uri is reachable and we can read/write
+ to it.
+
+ @param uri: The location to check
+ @type uri: C{URI}
+ @return: C{True} if the uri is reachable.
+ @rtype: C{bool}
+ """
+ if not uri_is_valid(uri):
+ raise NotImplementedError(
+ _("%s doesn't yet handle non local projects") % APPNAME)
+ return os.path.isfile(gst.uri_get_location(uri))
+
class PropertyChangeTracker(object, Signallable):
def __init__(self, timeline_object):
self.properties = {}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]