# # THIS FILE IS PART OF THE JOKOSHER PROJECT AND LICENSED UNDER THE GPL. SEE # THE 'COPYING' FILE FOR DETAILS # # ProjectManager.py # # Contains various helper classes for the Project class as well as # all of the loading and saving to and from project files. # #========================================================================= import urlparse, os, gzip, shutil, gst import Globals, Utils, UndoSystem, LevelsList, IncrementalSave import Project, Instrument, Event import xml.dom.minidom as xml import traceback import PlatformUtils def CreateNewProject(projecturi, name, author): """ Creates a new Project. Parameters: projecturi -- the filesystem location for the new Project. Currently, only file:// URIs are considered valid. name -- the name of the Project. author - the name of the Project's author. Returns: the newly created Project object. """ if name == "" or author == "" or projecturi == "": raise CreateProjectError(4) project = InitProjectLocation(projecturi, name) project.name = name project.author = author project.SaveProjectFile(project.projectfile) return project #_____________________________________________________________________ def InitProjectLocation(projecturi, name, project=None): """ Initialises the folder structure on disk for a Project. If no project is provided, a new one is created. Otherwise the given project is essentially moved to the new location. Parameters: projecturi -- the filesystem location for the new Project. Currently, only file:// URIs are considered valid. name -- the name of the Project. project -- the project to init, or None is a new project should be created. Returns: the given Project, or the newly created Project object. """ if name == "" or projecturi == "": raise CreateProjectError(4) (scheme, domain,folder, params, query, fragment) = urlparse.urlparse(projecturi, "file", False) folder = PlatformUtils.url2pathname(folder) if scheme != "file": # raise "The URI scheme used is invalid." message raise CreateProjectError(5) filename = name + ".jokosher" projectdir = os.path.join(folder, name) if not project: try: project = Project.Project() except gst.PluginNotFoundError, e: Globals.debug("Missing Gstreamer plugin:", e) raise CreateProjectError(6, str(e)) except Exception, e: Globals.debug("Could not initialize project object:", e) raise CreateProjectError(1) project.projectfile = os.path.join(projectdir, filename) project.audio_path = os.path.join(projectdir, "audio") project.levels_path = os.path.join(projectdir, "levels") if os.path.exists(projectdir): raise CreateProjectError(2) else: try: os.mkdir(projectdir) os.mkdir(project.audio_path) os.mkdir(project.levels_path) except: raise CreateProjectError(3) return project #_____________________________________________________________________ def ValidateProject(project): """ Checks that the Project is valid - i.e. that the files and images it references can be found. Parameters: project -- The project to validate. Returns: True -- the Project is valid. False -- the Project contains non-existant files and/or images. """ unknownfiles=[] unknownimages=[] for instr in project.instruments: for event in instr.events: file = event.GetAbsFile() if file and (not os.path.exists(file)) and (not file in unknownfiles): unknownfiles.append(file) if len(unknownfiles) > 0 or len(unknownimages) > 0: raise InvalidProjectError(unknownfiles,unknownimages) return True #_____________________________________________________________________ def LoadProjectFile(uri): """ Loads a Project from a saved file on disk. Parameters: uri -- the filesystem location of the Project file to load. Currently only file:// URIs are considered valid. Returns: the loaded Project object. """ (scheme, domain, projectfile, params, query, fragment) = urlparse.urlparse(uri, "file", False) if scheme != "file": # raise "The URI scheme used is invalid." message raise OpenProjectError(1, scheme) projectfile = PlatformUtils.url2pathname(projectfile) Globals.debug("Attempting to open:", projectfile) if not os.path.exists(projectfile): raise OpenProjectError(4, projectfile) try: try: gzipfile = gzip.GzipFile(projectfile, "r") doc = xml.parse(gzipfile) except IOError, e: if e.message == "Not a gzipped file": # starting from 0.10, we accept both gzipped xml and plain xml file_ = open(projectfile, "r") doc = xml.parse(file_) else: raise e except Exception, e: Globals.debug(e.__class__, e) # raise "This file doesn't unzip" message raise OpenProjectError(2, projectfile) project = Project.Project() project.projectfile = projectfile projectdir = os.path.split(projectfile)[0] project.audio_path = os.path.join(projectdir, "audio") project.levels_path = os.path.join(projectdir, "levels") try: if not os.path.exists(project.audio_path): os.mkdir(project.audio_path) if not os.path.exists(project.levels_path): os.mkdir(project.levels_path) except OSError: raise OpenProjectError(0) #only open projects with the proper version number version = None if doc and doc.firstChild: version = doc.firstChild.getAttribute("version") if JOKOSHER_VERSION_FUNCTIONS.has_key(version): loaderClass = JOKOSHER_VERSION_FUNCTIONS[version] Globals.debug("Loading project file version", version) try: loaderClass(project, doc) except: tb = traceback.format_exc() Globals.debug("Loading project failed", tb) raise OpenProjectError(5, tb) if version != Globals.VERSION: #if we're loading an old version copy the project so that it is not overwritten when the user clicks save withoutExt = os.path.splitext(projectfile)[0] shutil.copy(projectfile, "%s.%s.jokosher" % (withoutExt, version)) project.projectfile = projectfile return project else: # raise a "this project was created in an incompatible version of Jokosher" message raise OpenProjectError(3, version) #========================================================================= class _LoadZPOFile: def __init__(self, project, xmlDoc): """ Loads a project from a Jokosher 0.1 (Zero Point One) Project file into the given Project object using the given XML document. Parameters: project -- the Project instance to apply loaded properties to. xmlDoc -- the XML file document to read data from. """ params = xmlDoc.getElementsByTagName("Parameters")[0] Utils.LoadParametersFromXML(project, params) for instr in xmlDoc.getElementsByTagName("Instrument"): try: id = int(instr.getAttribute("id")) except ValueError: id = None i = Instrument.Instrument(project, None, None, None, id) self.LoadInstrument(i, instr) project.instruments.append(i) if i.isSolo: project.soloInstrCount += 1 #_____________________________________________________________________ def LoadInstrument(self, instr, xmlNode): """ Loads instrument properties from a Jokosher 0.1 XML node and saves them to the given Instrument instance. Parameters: instr -- the Instrument instance to apply loaded properties to. xmlNode -- the XML node to retreive data from. """ params = xmlNode.getElementsByTagName("Parameters")[0] Utils.LoadParametersFromXML(instr, params) #work around because in >0.2 instr.effects is a list not a string. instr.effects = [] for ev in xmlNode.getElementsByTagName("Event"): try: id = int(ev.getAttribute("id")) except ValueError: id = None e = Event.Event(instr, None, id) self.LoadEvent(e, ev) e.levels_file = e.GetFilename() + Event.Event.LEVELS_FILE_EXTENSION instr.events.append(e) pixbufFilename = os.path.basename(instr.pixbufPath) instr.instrType = os.path.splitext(pixbufFilename)[0] instr.pixbuf = Globals.getCachedInstrumentPixbuf(instr.instrType) if not instr.pixbuf: Globals.debug("Error, could not load image:", instr.instrType) #initialize the actuallyIsMuted variable instr.OnMute() #_____________________________________________________________________ def LoadEvent(self, event, xmlNode): """ Loads event properties from a Jokosher 0.1 XML node and saves then to the given Event instance. Parameters: event -- the Event instance to apply loaded properties to. xmlNode -- the XML node to retreive data from. """ params = xmlNode.getElementsByTagName("Parameters")[0] Utils.LoadParametersFromXML(event, params) try: xmlPoints = xmlNode.getElementsByTagName("FadePoints")[0] except IndexError: Globals.debug("Missing FadePoints in Event XML") else: for n in xmlPoints.childNodes: if n.nodeType == xml.Node.ELEMENT_NODE: pos = float(n.getAttribute("position")) value = float(n.getAttribute("fade")) event._Event__fadePointsDict[pos] = value event.GenerateWaveform() event._Event__UpdateAudioFadePoints() event.CreateFilesource() #_____________________________________________________________________ #========================================================================= class _LoadZPTFile: def __init__(self, project, xmlDoc): """ Loads a Jokosher version 0.2 (Zero Point Two) Project file into the given Project object using the given XML document. Parameters: project -- the Project instance to apply loaded properties to. xmlDoc -- the XML file document to read data from. """ self.project = project self.xmlDoc = xmlDoc params = self.xmlDoc.getElementsByTagName("Parameters")[0] Utils.LoadParametersFromXML(self.project, params) # Hack to set the transport mode self.project.transport.SetMode(self.project.transportMode) undoRedo = (("Undo", self.project._Project__savedUndoStack), ("Redo", self.project._Project__redoStack)) for tagName, stack in undoRedo: try: undo = self.xmlDoc.getElementsByTagName(tagName)[0] except IndexError: Globals.debug("No saved %s in project file" % tagName) else: for cmdNode in undo.childNodes: if cmdNode.nodeName == "Command": objectString = str(cmdNode.getAttribute("object")) functionString = str(cmdNode.getAttribute("function")) paramList = Utils.LoadListFromXML(cmdNode) functionString = ApplyUndoCompat(objectString, functionString, "0.2") undoAction = UndoSystem.AtomicUndoAction() undoAction.AddUndoCommand(objectString, functionString, paramList) stack.append(undoAction) for instrElement in self.xmlDoc.getElementsByTagName("Instrument"): try: id = int(instrElement.getAttribute("id")) except ValueError: id = None instr = Instrument.Instrument(self.project, None, None, None, id) self.LoadInstrument(instr, instrElement) self.project.instruments.append(instr) if instr.isSolo: self.project.soloInstrCount += 1 for instrElement in self.xmlDoc.getElementsByTagName("DeadInstrument"): try: id = int(instrElement.getAttribute("id")) except ValueError: id = None instr = Instrument.Instrument(self.project, None, None, None, id) self.LoadInstrument(instr, instrElement) self.project.graveyard.append(instr) instr.RemoveAndUnlinkPlaybackbin() #_____________________________________________________________________ def LoadInstrument(self, instr, xmlNode): """ Restores an Instrument from version 0.2 XML representation. Parameters: instr -- the Instrument instance to apply loaded properties to. xmlNode -- the XML node to retreive data from. """ params = xmlNode.getElementsByTagName("Parameters")[0] Utils.LoadParametersFromXML(instr, params) globaleffect = xmlNode.getElementsByTagName("GlobalEffect") for effect in globaleffect: elementname = str(effect.getAttribute("element")) Globals.debug("Loading effect:", elementname) gstElement = instr.AddEffect(elementname) propsdict = Utils.LoadDictionaryFromXML(effect) for key, value in propsdict.iteritems(): gstElement.set_property(key, value) for ev in xmlNode.getElementsByTagName("Event"): try: id = int(ev.getAttribute("id")) except ValueError: id = None event = Event.Event(instr, None, id) self.LoadEvent(event, ev) event.levels_file = event.GetFilename() + Event.Event.LEVELS_FILE_EXTENSION instr.events.append(event) for ev in xmlNode.getElementsByTagName("DeadEvent"): try: id = int(ev.getAttribute("id")) except ValueError: id = None event = Event.Event(instr, None, id) self.LoadEvent(event, ev, True) event.levels_file = event.GetFilename() + Event.Event.LEVELS_FILE_EXTENSION instr.graveyard.append(event) #load image from file based on unique type instr.pixbuf = Globals.getCachedInstrumentPixbuf(instr.instrType) if not instr.pixbuf: Globals.debug("Error, could not load image:", instr.instrType) # load pan level instr.panElement.set_property("panorama", instr.pan) #check if instrument is muted and setup accordingly instr.OnMute() #update the volume element with the newly loaded value instr.UpdateVolume() #_____________________________________________________________________ def LoadEvent(self, event, xmlNode, isDead=False): """ Restores an Event from its version 0.2 XML representation. Parameters: event -- the Event instance to apply loaded properties to. xmlNode -- the XML node to retreive data from. """ params = xmlNode.getElementsByTagName("Parameters")[0] Utils.LoadParametersFromXML(event, params) try: xmlPoints = xmlNode.getElementsByTagName("FadePoints")[0] except IndexError: Globals.debug("Missing FadePoints in Event XML") else: event._Event__fadePointsDict = Utils.LoadDictionaryFromXML(xmlPoints) if not isDead: #if event.isLoading or event.isRecording: # we have to always generate waveform because 0.10 uses different levels format event.GenerateWaveform() event._Event__UpdateAudioFadePoints() event.CreateFilesource() #_____________________________________________________________________ #========================================================================= class _LoadZPNFile(_LoadZPTFile): LOADING_VERSION = "0.9" def __init__(self, project, xmlDoc): """ Loads a Jokosher version 0.9 (Zero Point Nine) Project file into the given Project object using the given XML document. Parameters: project -- the Project instance to apply loaded properties to. xmlDoc -- the XML file document to read data from. """ self.project = project self.xmlDoc = xmlDoc params = self.xmlDoc.getElementsByTagName("Parameters")[0] Utils.LoadParametersFromXML(self.project, params) notesNode = self.xmlDoc.getElementsByTagName("Notes") if notesNode: notes = notesNode[0].getAttribute("text") # notes are encoded using repr() to preserver \n and \t. self.project.notes = Utils.StringUnRepr(notes) # Hack to set the transport mode self.project.transport.SetMode(self.project.transportMode) undoRedo = (("Undo", self.project._Project__savedUndoStack), ("Redo", self.project._Project__redoStack)) for tagName, stack in undoRedo: try: undo = self.xmlDoc.getElementsByTagName(tagName)[0] except IndexError: Globals.debug("No saved %s in project file" % tagName) else: for actionNode in undo.childNodes: if actionNode.nodeName == "Action": action = UndoSystem.AtomicUndoAction() self.LoadUndoAction(action, actionNode) stack.append(action) for instrElement in self.xmlDoc.getElementsByTagName("Instrument"): try: id = int(instrElement.getAttribute("id")) except ValueError: id = None instr = Instrument.Instrument(self.project, None, None, None, id) self.LoadInstrument(instr, instrElement) self.project.instruments.append(instr) if instr.isSolo: self.project.soloInstrCount += 1 for instrElement in self.xmlDoc.getElementsByTagName("DeadInstrument"): try: id = int(instrElement.getAttribute("id")) except ValueError: id = None instr = Instrument.Instrument(self.project, None, None, None, id) self.LoadInstrument(instr, instrElement) self.project.graveyard.append(instr) instr.RemoveAndUnlinkPlaybackbin() #_____________________________________________________________________ def LoadUndoAction(self, undoAction, xmlNode): """ Loads an AtomicUndoAction from an XML node. Parameters: undoAction -- the AtomicUndoAction instance to save the loaded commands to. node -- XML node from which the AtomicUndoAction is loaded. Should be an "" node. Returns: the loaded AtomicUndoAction object. """ for cmdNode in xmlNode.childNodes: if cmdNode.nodeName == "Command": objectString = str(cmdNode.getAttribute("object")) functionString = str(cmdNode.getAttribute("function")) paramList = Utils.LoadListFromXML(cmdNode) functionString = ApplyUndoCompat(objectString, functionString, self.LOADING_VERSION) undoAction.AddUndoCommand(objectString, functionString, paramList) #_____________________________________________________________________ #========================================================================= class _LoadZPTenFile(_LoadZPNFile): LOADING_VERSION = "0.10" def LoadEvent(self, event, xmlNode, isDead=False): """ Restores an Event from its version 0.10 XML representation. Parameters: event -- the Event instance to apply loaded properties to. xmlNode -- the XML node to retreive data from. """ params = xmlNode.getElementsByTagName("Parameters")[0] Utils.LoadParametersFromXML(event, params) try: xmlPoints = xmlNode.getElementsByTagName("FadePoints")[0] except IndexError: Globals.debug("Missing FadePoints in Event XML") else: event._Event__fadePointsDict = Utils.LoadDictionaryFromXML(xmlPoints) if not isDead: if event.isLoading or event.isRecording: event.GenerateWaveform() else: levels_path = event.GetAbsLevelsFile() try: event.levels_list.fromfile(levels_path) except LevelsList.CorruptFileError: Globals.debug("Cannot load levels from file", levels_path) if not event.levels_list: event.GenerateWaveform() event._Event__UpdateAudioFadePoints() event.CreateFilesource() #_____________________________________________________________________ #========================================================================= class OpenProjectError(EnvironmentError): """ This class will get created when a opening a Project fails. It's used for handling errors. """ #_____________________________________________________________________ def __init__(self, errno, info = None): """ Creates a new instance of OpenProjectError. Parameters: errno -- number indicating the type of error: 1 = invalid uri passed for the Project file. 2 = unable to unzip the Project. 3 = Project created by a different version of Jokosher. 4 = Project file doesn't exist. 5 = Loading process faild with traceback info -- version of Jokosher that created the Project. Will be present only along with error #3. OR traceback of thrown exception, which will be present with #5 """ EnvironmentError.__init__(self) self.info = info self.errno = errno #_____________________________________________________________________ #========================================================================= class CreateProjectError(Exception): """ This class will get created when creating a Project fails. It's used for handling errors. """ #_____________________________________________________________________ def __init__(self, errno, message=None): """ Creates a new instance of CreateProjectError. Parameters: errno -- number indicating the type of error: 1 = unable to create a Project object. 2 = path for Project file already exists. 3 = unable to create file. (Invalid permissions, read-only, or the disk is full). 4 = invalid path, name or author. 5 = invalid uri passed for the Project file. 6 = unable to load a particular gstreamer plugin (message will be the plugin's name) message -- a string with more specific information about the error """ Exception.__init__(self) self.errno = errno self.message = message #_____________________________________________________________________ #========================================================================= class InvalidProjectError(Exception): """ This class will get created when there's an invalid Project. It's used for handling errors. """ #_____________________________________________________________________ def __init__(self, missingfiles, missingimages): """ Creates a new instance of InvalidProjectError. Parameters: missingfiles -- filenames of the missing files. missingimages -- filenames of the missing images. """ Exception.__init__(self) self.files=missingfiles self.images=missingimages #_____________________________________________________________________ #========================================================================= class ProjectExportException(Exception): """ This class will get created when there are problems with the soundcard inputs. It's used for handling errors. Error Codes: MISSING_ELEMENT - incorrect element name or element not installed. INVALID_ENCODE_BIN - invalid bin description; invalid syntax, invalid properties, incompatible caps, etc. """ MISSING_ELEMENT, INVALID_ENCODE_BIN = range(2) #_____________________________________________________________________ def __init__(self, errno, message): """ Parameters: errno -- number indicating the type of error. See error codes above. """ Exception.__init__(self) self.errno = errno self.message = message #_____________________________________________________________________ #========================================================================= def ApplyUndoCompat(objectString, functionString, version): if UNDO_COMPAT_DICT.has_key(version): compact_dict = UNDO_COMPAT_DICT[version] tuple_ = (objectString[0], functionString) if compact_dict.has_key(tuple_): return compact_dict[tuple_] return functionString #========================================================================= JOKOSHER_VERSION_FUNCTIONS = { "0.1" : _LoadZPOFile, "0.2" : _LoadZPTFile, "0.9" : _LoadZPNFile, "1.0" : _LoadZPNFile, # 1.0 was never used in a release, and it identical to 0.9 "0.10" : _LoadZPTenFile, "0.11" : _LoadZPTenFile, # 0.11 is identical to 0.10, exception for project notes, whose presence can be detected "0.11.1" : _LoadZPTenFile, } zero_nine_compat = {("E", "Move") : "_Compat09_Move"} zero_two_compat = { ("E", "Split") : "_Compat02_Split", ("E", "Join") : "_Compat02_Join", ("E", "UndoTrim") : "_Compat02_UndoTrim", } #we can't import undo from 0.1 because the storage of undo was revamped for version 0.2 #all the compat info from newer versions, also applies to older versions for key, value in zero_nine_compat.iteritems(): zero_two_compat.setdefault(key, value) UNDO_COMPAT_DICT = {"0.2" : zero_two_compat, "0.9" : zero_nine_compat} #=========================================================================