OpenShot Video Editor  2.0.0
project_data.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file listens to changes, and updates the primary project data
5 # @author Noah Figg <eggmunkee@hotmail.com>
6 # @author Jonathan Thomas <jonathan@openshot.org>
7 # @author Olivier Girard <eolinwen@gmail.com>
8 #
9 # @section LICENSE
10 #
11 # Copyright (c) 2008-2016 OpenShot Studios, LLC
12 # (http://www.openshotstudios.com). This file is part of
13 # OpenShot Video Editor (http://www.openshot.org), an open-source project
14 # dedicated to delivering high quality video editing and animation solutions
15 # to the world.
16 #
17 # OpenShot Video Editor is free software: you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation, either version 3 of the License, or
20 # (at your option) any later version.
21 #
22 # OpenShot Video Editor is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
26 #
27 # You should have received a copy of the GNU General Public License
28 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
29 #
30 
31 import os
32 import random
33 import copy
34 import shutil
35 import glob
36 
37 from classes.json_data import JsonDataStore
38 from classes.updates import UpdateInterface
39 from classes import info, settings
40 from classes.logger import log
41 
42 
43 ##
44 # This class allows advanced searching of data structure, implements changes interface
45 class ProjectDataStore(JsonDataStore, UpdateInterface):
46 
47  def __init__(self):
48  JsonDataStore.__init__(self)
49  self.data_type = "project data" # Used in error messages
50  self.default_project_filepath = os.path.join(info.PATH, 'settings', '_default.project')
51 
52  # Set default filepath to user's home folder
53  self.current_filepath = None
54 
55  # Track changes after save
56  self.has_unsaved_changes = False
57 
58  # Load default project data on creation
59  self.new()
60 
61  ##
62  # Returns if project data Has unsaved changes
63  def needs_save(self):
64  return self.has_unsaved_changes
65 
66  ##
67  # Get copied value of a given key in data store
68  def get(self, key):
69 
70  # Verify key is valid type
71  if not isinstance(key, list):
72  log.warning("get() key must be a list. key: {}".format(key))
73  return None
74  if not key:
75  log.warning("Cannot get empty key.")
76  return None
77 
78  # Get reference to internal data structure
79  obj = self._data
80 
81  # Iterate through key list finding sub-objects either by name or by an object match criteria such as {"id":"ADB34"}.
82  for key_index in range(len(key)):
83  key_part = key[key_index]
84 
85  # Key_part must be a string or dictionary
86  if not isinstance(key_part, dict) and not isinstance(key_part, str):
87  log.error("Unexpected key part type: {}".format(type(key_part).__name__))
88  return None
89 
90  # If key_part is a dictionary and obj is a list or dict, each key is tested as a property of the items in the current object
91  # in the project data structure, and the first match is returned.
92  if isinstance(key_part, dict) and isinstance(obj, list):
93  # Overall status of finding a matching sub-object
94  found = False
95  # Loop through each item in object to find match
96  for item_index in range(len(obj)):
97  item = obj[item_index]
98  # True until something disqualifies this as a match
99  match = True
100  # Check each key in key_part dictionary and if not found to be equal as a property in item, move on to next item in list
101  for subkey in key_part.keys():
102  # Get each key in dictionary (i.e. "id", "layer", etc...)
103  subkey = subkey.lower()
104  # If object is missing the key or the values differ, then it doesn't match.
105  if not (subkey in item and item[subkey] == key_part[subkey]):
106  match = False
107  break
108  # If matched, set key_part to index of list or dict and stop loop
109  if match:
110  found = True
111  obj = item
112  break
113  # No match found, return None
114  if not found:
115  return None
116 
117  # If key_part is a string, homogenize to lower case for comparisons
118  if isinstance(key_part, str):
119  key_part = key_part.lower()
120 
121  # Check current obj type (should be dictionary)
122  if not isinstance(obj, dict):
123  log.warn(
124  "Invalid project data structure. Trying to use a key on a non-dictionary object. Key part: {} (\"{}\").\nKey: {}".format(
125  (key_index), key_part, key))
126  return None
127 
128  # If next part of path isn't in current dictionary, return failure
129  if not key_part in obj:
130  log.warn("Key not found in project. Mismatch on key part {} (\"{}\").\nKey: {}".format((key_index),
131  key_part,
132  key))
133  return None
134 
135  # Get the matching item
136  obj = obj[key_part]
137 
138  # After processing each key, we've found object, return copy of it
139  return copy.deepcopy(obj)
140 
141  ##
142  # Prevent calling JsonDataStore set() method. It is not allowed in ProjectDataStore, as changes come from UpdateManager.
143  def set(self, key, value):
144  raise Exception("ProjectDataStore.set() is not allowed. Changes must route through UpdateManager.")
145 
146  ##
147  # Store setting, but adding isn't allowed. All possible settings must be in default settings file.
148  def _set(self, key, values=None, add=False, partial_update=False, remove=False):
149 
150  log.info(
151  "_set key: {} values: {} add: {} partial: {} remove: {}".format(key, values, add, partial_update, remove))
152  parent, my_key = None, ""
153 
154  # Verify key is valid type
155  if not isinstance(key, list):
156  log.warning("_set() key must be a list. key: {}".format(key))
157  return None
158  if not key:
159  log.warning("Cannot set empty key.")
160  return None
161 
162  # Get reference to internal data structure
163  obj = self._data
164 
165  # Iterate through key list finding sub-objects either by name or by an object match criteria such as {"id":"ADB34"}.
166  for key_index in range(len(key)):
167  key_part = key[key_index]
168 
169  # Key_part must be a string or dictionary
170  if not isinstance(key_part, dict) and not isinstance(key_part, str):
171  log.error("Unexpected key part type: {}".format(type(key_part).__name__))
172  return None
173 
174  # If key_part is a dictionary and obj is a list or dict, each key is tested as a property of the items in the current object
175  # in the project data structure, and the first match is returned.
176  if isinstance(key_part, dict) and isinstance(obj, list):
177  # Overall status of finding a matching sub-object
178  found = False
179  # Loop through each item in object to find match
180  for item_index in range(len(obj)):
181  item = obj[item_index]
182  # True until something disqualifies this as a match
183  match = True
184  # Check each key in key_part dictionary and if not found to be equal as a property in item, move on to next item in list
185  for subkey in key_part.keys():
186  # Get each key in dictionary (i.e. "id", "layer", etc...)
187  subkey = subkey.lower()
188  # If object is missing the key or the values differ, then it doesn't match.
189  if not (subkey in item and item[subkey] == key_part[subkey]):
190  match = False
191  break
192  # If matched, set key_part to index of list or dict and stop loop
193  if match:
194  found = True
195  obj = item
196  my_key = item_index
197  break
198  # No match found, return None
199  if not found:
200  return None
201 
202 
203  # If key_part is a string, homogenize to lower case for comparisons
204  if isinstance(key_part, str):
205  key_part = key_part.lower()
206 
207  # Check current obj type (should be dictionary)
208  if not isinstance(obj, dict):
209  return None
210 
211  # If next part of path isn't in current dictionary, return failure
212  if not key_part in obj:
213  log.warn("Key not found in project. Mismatch on key part {} (\"{}\").\nKey: {}".format((key_index),
214  key_part,
215  key))
216  return None
217 
218  # Get sub-object based on part key as new object, continue to next part
219  obj = obj[key_part]
220  my_key = key_part
221 
222 
223  # Set parent to the last set obj (if not final iteration)
224  if key_index < (len(key) - 1) or key_index == 0:
225  parent = obj
226 
227 
228  # After processing each key, we've found object and parent, return former value/s on update
229  ret = copy.deepcopy(obj)
230 
231  # Apply the correct action to the found item
232  if remove:
233  del parent[my_key]
234 
235  else:
236 
237  # Add or Full Update
238  # For adds to list perform an insert to index or the end if not specified
239  if add and isinstance(parent, list):
240  # log.info("adding to list")
241  parent.append(values)
242 
243  # Otherwise, set the given index
244  elif isinstance(values, dict):
245  # Update existing dictionary value
246  obj.update(values)
247 
248  else:
249 
250  # Update root string
251  self._data[my_key] = values
252 
253  # Return the previous value to the matching item (used for history tracking)
254  return ret
255 
256  # Load default project data
257  ##
258  # Try to load default project settings file, will raise error on failure
259  def new(self):
260  import openshot
261  self._data = self.read_from_file(self.default_project_filepath)
262  self.current_filepath = None
263  self.has_unsaved_changes = False
264 
265  # Get default profile
267  default_profile = s.get("default-profile")
268 
269  # Loop through profiles
270  for profile_folder in [info.USER_PROFILES_PATH, info.PROFILES_PATH]:
271  for file in os.listdir(profile_folder):
272  # Load Profile and append description
273  profile_path = os.path.join(profile_folder, file)
274  profile = openshot.Profile(profile_path)
275 
276  if default_profile == profile.info.description:
277  log.info("Setting default profile to %s" % profile.info.description)
278 
279  # Update default profile
280  self._data["profile"] = profile.info.description
281  self._data["width"] = profile.info.width
282  self._data["height"] = profile.info.height
283  self._data["fps"] = {"num" : profile.info.fps.num, "den" : profile.info.fps.den}
284 
285  # Get the default audio settings for the timeline (and preview playback)
286  default_sample_rate = int(s.get("default-samplerate"))
287  default_channel_ayout = s.get("default-channellayout")
288 
289  channels = 2
290  channel_layout = openshot.LAYOUT_STEREO
291  if default_channel_ayout == "LAYOUT_MONO":
292  channels = 1
293  channel_layout = openshot.LAYOUT_MONO
294  elif default_channel_ayout == "LAYOUT_STEREO":
295  channels = 2
296  channel_layout = openshot.LAYOUT_STEREO
297  elif default_channel_ayout == "LAYOUT_SURROUND":
298  channels = 3
299  channel_layout = openshot.LAYOUT_SURROUND
300  elif default_channel_ayout == "LAYOUT_5POINT1":
301  channels = 6
302  channel_layout = openshot.LAYOUT_5POINT1
303  elif default_channel_ayout == "LAYOUT_7POINT1":
304  channels = 8
305  channel_layout = openshot.LAYOUT_7POINT1
306 
307  # Set default samplerate and channels
308  self._data["sample_rate"] = default_sample_rate
309  self._data["channels"] = channels
310  self._data["channel_layout"] = channel_layout
311 
312  ##
313  # Load project from file
314  def load(self, file_path):
315 
316  self.new()
317 
318  if file_path:
319  # Default project data
320  default_project = self._data
321 
322  try:
323  # Attempt to load v2.X project file
324  project_data = self.read_from_file(file_path)
325 
326  except Exception as ex:
327  try:
328  # Attempt to load legacy project file (v1.X version)
329  project_data = self.read_legacy_project_file(file_path)
330 
331  except Exception as ex:
332  # Project file not recognized as v1.X or v2.X, bubble up error
333  raise ex
334 
335  # Merge default and project settings, excluding settings not in default.
336  self._data = self.merge_settings(default_project, project_data)
337 
338  # On success, save current filepath
339  self.current_filepath = file_path
340 
341  # Convert all paths back to absolute
343 
344  # Copy any project thumbnails to main THUMBNAILS folder
345  loaded_project_folder = os.path.dirname(self.current_filepath)
346  project_thumbnails_folder = os.path.join(loaded_project_folder, "thumbnail")
347  if os.path.exists(project_thumbnails_folder):
348  # Remove thumbnail path
349  shutil.rmtree(info.THUMBNAIL_PATH, True)
350 
351  # Copy project thumbnails folder
352  shutil.copytree(project_thumbnails_folder, info.THUMBNAIL_PATH)
353 
354  # Add to recent files setting
355  self.add_to_recent_files(file_path)
356 
357  # Upgrade any data structures
359 
360  # Get app, and distribute all project data through update manager
361  from classes.app import get_app
362  get_app().updates.load(self._data)
363 
364  # Clear needs save flag
365  self.has_unsaved_changes = False
366 
367  ##
368  # Attempt to read a legacy version 1.x openshot project file
369  def read_legacy_project_file(self, file_path):
370  import sys, pickle
371  from classes.query import File, Track, Clip, Transition
372  from classes.app import get_app
373  import openshot
374 
375  try:
376  import json
377  except ImportError:
378  import simplejson as json
379 
380  # Get translation method
381  _ = get_app()._tr
382 
383  # Append version info
384  v = openshot.GetVersion()
385  project_data = {}
386  project_data["version"] = { "openshot-qt" : info.VERSION,
387  "libopenshot" : v.ToString() }
388 
389  # Get FPS from project
390  from classes.app import get_app
391  fps = get_app().project.get(["fps"])
392  fps_float = float(fps["num"]) / float(fps["den"])
393 
394  # Import legacy openshot classes (from version 1.X)
395  from classes.legacy.openshot import classes as legacy_classes
396  from classes.legacy.openshot.classes import project as legacy_project
397  from classes.legacy.openshot.classes import sequences as legacy_sequences
398  from classes.legacy.openshot.classes import track as legacy_track
399  from classes.legacy.openshot.classes import clip as legacy_clip
400  from classes.legacy.openshot.classes import keyframe as legacy_keyframe
401  from classes.legacy.openshot.classes import files as legacy_files
402  from classes.legacy.openshot.classes import transition as legacy_transition
403  sys.modules['openshot.classes'] = legacy_classes
404  sys.modules['classes.project'] = legacy_project
405  sys.modules['classes.sequences'] = legacy_sequences
406  sys.modules['classes.track'] = legacy_track
407  sys.modules['classes.clip'] = legacy_clip
408  sys.modules['classes.keyframe'] = legacy_keyframe
409  sys.modules['classes.files'] = legacy_files
410  sys.modules['classes.transition'] = legacy_transition
411 
412  # Keep track of files that failed to load
413  failed_files = []
414 
415  with open(file_path.encode('UTF-8'), 'rb') as f:
416  try:
417  # Unpickle legacy openshot project file
418  v1_data = pickle.load(f, fix_imports=True)
419  file_lookup = {}
420 
421  # Loop through files
422  for item in v1_data.project_folder.items:
423  # Is this item a File (i.e. ignore folders)
424  if isinstance(item, legacy_files.OpenShotFile):
425  # Create file
426  try:
427  clip = openshot.Clip(item.name)
428  reader = clip.Reader()
429  file_data = json.loads(reader.Json())
430 
431  # Determine media type
432  if file_data["has_video"] and not self.is_image(file_data):
433  file_data["media_type"] = "video"
434  elif file_data["has_video"] and self.is_image(file_data):
435  file_data["media_type"] = "image"
436  elif file_data["has_audio"] and not file_data["has_video"]:
437  file_data["media_type"] = "audio"
438 
439  # Save new file to the project data
440  file = File()
441  file.data = file_data
442  file.save()
443 
444  # Keep track of new ids and old ids
445  file_lookup[item.unique_id] = file
446 
447  except:
448  # Handle exception quietly
449  msg = ("%s is not a valid video, audio, or image file." % item.name)
450  log.error(msg)
451  failed_files.append(item.name)
452 
453  # Delete all tracks
454  track_list = copy.deepcopy(Track.filter())
455  for track in track_list:
456  track.delete()
457 
458  # Create new tracks
459  track_counter = 0
460  for legacy_t in reversed(v1_data.sequences[0].tracks):
461  t = Track()
462  t.data = {"number": track_counter, "y": 0, "label": legacy_t.name}
463  t.save()
464 
465  track_counter += 1
466 
467  # Loop through clips
468  track_counter = 0
469  for sequence in v1_data.sequences:
470  for track in reversed(sequence.tracks):
471  for clip in track.clips:
472  # Get associated file for this clip
473  if clip.file_object.unique_id in file_lookup.keys():
474  file = file_lookup[clip.file_object.unique_id]
475  else:
476  # Skip missing file
477  log.info("Skipping importing missing file: %s" % clip.file_object.unique_id)
478  continue
479 
480  # Create clip
481  if (file.data["media_type"] == "video" or file.data["media_type"] == "image"):
482  # Determine thumb path
483  thumb_path = os.path.join(info.THUMBNAIL_PATH, "%s.png" % file.data["id"])
484  else:
485  # Audio file
486  thumb_path = os.path.join(info.PATH, "images", "AudioThumbnail.png")
487 
488  # Get file name
489  path, filename = os.path.split(file.data["path"])
490 
491  # Convert path to the correct relative path (based on this folder)
492  file_path = file.absolute_path()
493 
494  # Create clip object for this file
495  c = openshot.Clip(file_path)
496 
497  # Append missing attributes to Clip JSON
498  new_clip = json.loads(c.Json())
499  new_clip["file_id"] = file.id
500  new_clip["title"] = filename
501  new_clip["image"] = thumb_path
502 
503  # Check for optional start and end attributes
504  new_clip["start"] = clip.start_time
505  new_clip["end"] = clip.end_time
506  new_clip["position"] = clip.position_on_track
507  new_clip["layer"] = track_counter
508 
509  # Clear alpha (if needed)
510  if clip.video_fade_in or clip.video_fade_out:
511  new_clip["alpha"]["Points"] = []
512 
513  # Video Fade IN
514  if clip.video_fade_in:
515  # Add keyframes
516  start = openshot.Point(clip.start_time * fps_float, 0.0, openshot.BEZIER)
517  start_object = json.loads(start.Json())
518  end = openshot.Point((clip.start_time + clip.video_fade_in_amount) * fps_float, 1.0, openshot.BEZIER)
519  end_object = json.loads(end.Json())
520  new_clip["alpha"]["Points"].append(start_object)
521  new_clip["alpha"]["Points"].append(end_object)
522 
523  # Video Fade OUT
524  if clip.video_fade_out:
525  # Add keyframes
526  start = openshot.Point((clip.end_time - clip.video_fade_out_amount) * fps_float, 1.0, openshot.BEZIER)
527  start_object = json.loads(start.Json())
528  end = openshot.Point(clip.end_time * fps_float, 0.0, openshot.BEZIER)
529  end_object = json.loads(end.Json())
530  new_clip["alpha"]["Points"].append(start_object)
531  new_clip["alpha"]["Points"].append(end_object)
532 
533  # Clear Audio (if needed)
534  if clip.audio_fade_in or clip.audio_fade_out:
535  new_clip["volume"]["Points"] = []
536  else:
537  p = openshot.Point(1, clip.volume / 100.0, openshot.BEZIER)
538  p_object = json.loads(p.Json())
539  new_clip["volume"] = { "Points" : [p_object]}
540 
541  # Audio Fade IN
542  if clip.audio_fade_in:
543  # Add keyframes
544  start = openshot.Point(clip.start_time * fps_float, 0.0, openshot.BEZIER)
545  start_object = json.loads(start.Json())
546  end = openshot.Point((clip.start_time + clip.video_fade_in_amount) * fps_float, clip.volume / 100.0, openshot.BEZIER)
547  end_object = json.loads(end.Json())
548  new_clip["volume"]["Points"].append(start_object)
549  new_clip["volume"]["Points"].append(end_object)
550 
551  # Audio Fade OUT
552  if clip.audio_fade_out:
553  # Add keyframes
554  start = openshot.Point((clip.end_time - clip.video_fade_out_amount) * fps_float, clip.volume / 100.0, openshot.BEZIER)
555  start_object = json.loads(start.Json())
556  end = openshot.Point(clip.end_time * fps_float, 0.0, openshot.BEZIER)
557  end_object = json.loads(end.Json())
558  new_clip["volume"]["Points"].append(start_object)
559  new_clip["volume"]["Points"].append(end_object)
560 
561  # Save clip
562  clip_object = Clip()
563  clip_object.data = new_clip
564  clip_object.save()
565 
566  # Loop through transitions
567  for trans in track.transitions:
568  # Fix default transition
569  if not trans.resource or not os.path.exists(trans.resource):
570  trans.resource = os.path.join(info.PATH, "transitions", "common", "fade.svg")
571 
572  # Open up QtImageReader for transition Image
573  transition_reader = openshot.QtImageReader(trans.resource)
574 
575  trans_begin_value = 1.0
576  trans_end_value = -1.0
577  if trans.reverse:
578  trans_begin_value = -1.0
579  trans_end_value = 1.0
580 
581  brightness = openshot.Keyframe()
582  brightness.AddPoint(1, trans_begin_value, openshot.BEZIER)
583  brightness.AddPoint(trans.length * fps_float, trans_end_value, openshot.BEZIER)
584  contrast = openshot.Keyframe(trans.softness * 10.0)
585 
586  # Create transition dictionary
587  transitions_data = {
588  "id": get_app().project.generate_id(),
589  "layer": track_counter,
590  "title": "Transition",
591  "type": "Mask",
592  "position": trans.position_on_track,
593  "start": 0,
594  "end": trans.length,
595  "brightness": json.loads(brightness.Json()),
596  "contrast": json.loads(contrast.Json()),
597  "reader": json.loads(transition_reader.Json()),
598  "replace_image": False
599  }
600 
601  # Save transition
602  t = Transition()
603  t.data = transitions_data
604  t.save()
605 
606  # Increment track counter
607  track_counter += 1
608 
609  except Exception as ex:
610  # Error parsing legacy contents
611  msg = _("Failed to load project file %s: %s" % (file_path, ex))
612  log.error(msg)
613  raise Exception(msg)
614 
615  # Show warning if some files failed to load
616  if failed_files:
617  # Throw exception
618  raise Exception(_("Failed to load the following files:\n%s" % ", ".join(failed_files)))
619 
620  # Return mostly empty project_data dict (with just the current version #)
621  log.info("Successfully loaded legacy project file: %s" % file_path)
622  return project_data
623 
624  def is_image(self, file):
625  path = file["path"].lower()
626 
627  if path.endswith((".jpg", ".jpeg", ".png", ".bmp", ".svg", ".thm", ".gif", ".bmp", ".pgm", ".tif", ".tiff")):
628  return True
629  else:
630  return False
631 
632  ##
633  # Fix any issues with old project files (if any)
635  openshot_version = self._data["version"]["openshot-qt"]
636  libopenshot_version = self._data["version"]["libopenshot"]
637 
638  log.info(openshot_version)
639  log.info(libopenshot_version)
640 
641  if openshot_version == "0.0.0":
642  # If version = 0.0.0, this is the beta of OpenShot
643  # Fix alpha values (they are now flipped)
644  for clip in self._data["clips"]:
645  # Loop through keyframes for alpha
646  for point in clip["alpha"]["Points"]:
647  # Flip the alpha value
648  if "co" in point:
649  point["co"]["Y"] = 1.0 - point["co"]["Y"]
650  if "handle_left" in point:
651  point["handle_left"]["Y"] = 1.0 - point["handle_left"]["Y"]
652  if "handle_right" in point:
653  point["handle_right"]["Y"] = 1.0 - point["handle_right"]["Y"]
654 
655  ##
656  # Save project file to disk
657  def save(self, file_path, move_temp_files=True, make_paths_relative=True):
658  import openshot
659 
660  # Move all temp files (i.e. Blender animations) to the project folder
661  if move_temp_files:
662  self.move_temp_paths_to_project_folder(file_path)
663 
664  # Convert all file paths to relative based on this new project file's directory
665  if make_paths_relative:
666  self.convert_paths_to_relative(file_path)
667 
668  # Append version info
669  v = openshot.GetVersion()
670  self._data["version"] = { "openshot-qt" : info.VERSION,
671  "libopenshot" : v.ToString() }
672 
673  # Try to save project settings file, will raise error on failure
674  self.write_to_file(file_path, self._data)
675 
676  # On success, save current filepath
677  self.current_filepath = file_path
678 
679  # Convert all paths back to absolute
680  if make_paths_relative:
682 
683  # Add to recent files setting
684  self.add_to_recent_files(file_path)
685 
686  # Track unsaved changes
687  self.has_unsaved_changes = False
688 
689  ##
690  # Move all temp files (such as Thumbnails, Titles, and Blender animations) to the project folder.
691  def move_temp_paths_to_project_folder(self, file_path):
692  try:
693  # Get project folder
694  new_project_folder = os.path.dirname(file_path)
695  new_thumbnails_folder = os.path.join(new_project_folder, "thumbnail")
696 
697  # Create project thumbnails folder
698  if not os.path.exists(new_thumbnails_folder):
699  os.mkdir(new_thumbnails_folder)
700 
701  # Copy all thumbnails to project
702  for filename in glob.glob(os.path.join(info.THUMBNAIL_PATH, '*.*')):
703  shutil.copy(filename, new_thumbnails_folder)
704 
705  # Loop through each file
706  for file in self._data["files"]:
707  path = file["path"]
708 
709  # Find any temp BLENDER file paths
710  if info.BLENDER_PATH in path:
711  log.info("Temp blender file path detected in file")
712 
713  # Get folder of file
714  folder_path, file_name = os.path.split(path)
715  parent_path, folder_name = os.path.split(folder_path)
716  # Update path to new folder
717  path = os.path.join(new_project_folder, folder_name)
718  # Copy temp folder to project folder
719  shutil.copytree(folder_path, path)
720 
721  # Update paths in project to new location
722  file["path"] = os.path.join(path, file_name)
723 
724  # Loop through each clip
725  for clip in self._data["clips"]:
726  path = clip["reader"]["path"]
727 
728  # Find any temp BLENDER file paths
729  if info.BLENDER_PATH in path:
730  log.info("Temp blender file path detected in clip")
731 
732  # Get folder of file
733  folder_path, file_name = os.path.split(path)
734  parent_path, folder_name = os.path.split(folder_path)
735  # Update path to new folder
736  path = os.path.join(new_project_folder, folder_name)
737 
738  # Update paths in project to new location
739  clip["reader"]["path"] = os.path.join(path, file_name)
740 
741  # Loop through each file
742  for clip in self._data["clips"]:
743  path = clip["image"]
744 
745  # Find any temp BLENDER file paths
746  if info.BLENDER_PATH in path:
747  log.info("Temp blender file path detected in clip thumbnail")
748 
749  # Get folder of file
750  folder_path, file_name = os.path.split(path)
751  parent_path, folder_name = os.path.split(folder_path)
752  # Update path to new folder
753  path = os.path.join(new_project_folder, folder_name)
754 
755  # Update paths in project to new location
756  clip["image"] = os.path.join(path, file_name)
757 
758  except Exception as ex:
759  log.error("Error while moving temp files into project folder: %s" % str(ex))
760 
761  ##
762  # Add this project to the recent files list
763  def add_to_recent_files(self, file_path):
764 
766  recent_projects = s.get("recent_projects")
767 
768  # Remove existing project
769  if file_path in recent_projects:
770  recent_projects.remove(file_path)
771 
772  # Remove oldest item (if needed)
773  if len(recent_projects) > 10:
774  del recent_projects[0]
775 
776  # Append file path to end of recent files
777  recent_projects.append(file_path)
778 
779  # Save setting
780  s.set("recent_projects", recent_projects)
781 
782  ##
783  # Convert all paths relative to this filepath
784  def convert_paths_to_relative(self, file_path):
785  try:
786  # Get project folder
787  existing_project_folder = None
788  if self.current_filepath:
789  existing_project_folder = os.path.dirname(self.current_filepath)
790  new_project_folder = os.path.dirname(file_path)
791 
792  # Loop through each file
793  for file in self._data["files"]:
794  path = file["path"]
795  # Find absolute path of file (if needed)
796  if not os.path.isabs(path):
797  # Convert path to the correct relative path (based on the existing folder)
798  path = os.path.abspath(os.path.join(existing_project_folder, path))
799 
800  # Convert absolute path to relavite
801  file["path"] = os.path.relpath(path, new_project_folder)
802 
803  # Loop through each clip
804  for clip in self._data["clips"]:
805  # Update reader path
806  path = clip["reader"]["path"]
807  # Find absolute path of file (if needed)
808  if not os.path.isabs(path):
809  # Convert path to the correct relative path (based on the existing folder)
810  path = os.path.abspath(os.path.join(existing_project_folder, path))
811  # Convert absolute path to relavite
812  clip["reader"]["path"] = os.path.relpath(path, new_project_folder)
813 
814  # Update clip image path
815  path = clip["image"]
816  # Find absolute path of file (if needed)
817  if not os.path.isabs(path):
818  # Convert path to the correct relative path (based on the existing folder)
819  path = os.path.abspath(os.path.join(existing_project_folder, path))
820  # Convert absolute path to relavite
821  clip["image"] = os.path.relpath(path, new_project_folder)
822 
823  # Loop through each transition
824  for effect in self._data["effects"]:
825  # Update reader path
826  path = effect["reader"]["path"]
827 
828  # Determine if this path is the official transition path
829  folder_path, file_path = os.path.split(path)
830  if os.path.join(info.PATH, "transitions") in folder_path:
831  # Yes, this is an OpenShot transitions
832  folder_path, category_path = os.path.split(folder_path)
833 
834  # Convert path to @transitions/ path
835  effect["reader"]["path"] = os.path.join("@transitions", category_path, file_path)
836  continue
837 
838  # Find absolute path of file (if needed)
839  if not os.path.isabs(path):
840  # Convert path to the correct relative path (based on the existing folder)
841  path = os.path.abspath(os.path.join(existing_project_folder, path))
842  # Convert absolute path to relavite
843  effect["reader"]["path"] = os.path.relpath(path, new_project_folder)
844 
845  except Exception as ex:
846  log.error("Error while converting absolute paths to relative paths: %s" % str(ex))
847 
848 
849  ##
850  # Convert all paths to absolute
852  try:
853  # Get project folder
854  existing_project_folder = None
855  if self.current_filepath:
856  existing_project_folder = os.path.dirname(self.current_filepath)
857 
858  # Loop through each file
859  for file in self._data["files"]:
860  path = file["path"]
861  # Find absolute path of file (if needed)
862  if not os.path.isabs(path):
863  # Convert path to the correct relative path (based on the existing folder)
864  path = os.path.abspath(os.path.join(existing_project_folder, path))
865 
866  # Convert absolute path to relavite
867  file["path"] = path
868 
869  # Loop through each clip
870  for clip in self._data["clips"]:
871  # Update reader path
872  path = clip["reader"]["path"]
873  # Find absolute path of file (if needed)
874  if not os.path.isabs(path):
875  # Convert path to the correct relative path (based on the existing folder)
876  path = os.path.abspath(os.path.join(existing_project_folder, path))
877  # Convert absolute path to relavite
878  clip["reader"]["path"] = path
879 
880  # Update clip image path
881  path = clip["image"]
882  # Find absolute path of file (if needed)
883  if not os.path.isabs(path):
884  # Convert path to the correct relative path (based on the existing folder)
885  path = os.path.abspath(os.path.join(existing_project_folder, path))
886  # Convert absolute path to relavite
887  clip["image"] = path
888 
889  # Loop through each transition
890  for effect in self._data["effects"]:
891  # Update reader path
892  path = effect["reader"]["path"]
893 
894  # Determine if @transitions path is found
895  if "@transitions" in path:
896  path = path.replace("@transitions", os.path.join(info.PATH, "transitions"))
897 
898  # Find absolute path of file (if needed)
899  if not os.path.isabs(path):
900  # Convert path to the correct relative path (based on the existing folder)
901  path = os.path.abspath(os.path.join(existing_project_folder, path))
902  # Convert absolute path to relavite
903  effect["reader"]["path"] = path
904 
905  except Exception as ex:
906  log.error("Error while converting relative paths to absolute paths: %s" % str(ex))
907 
908  ##
909  # This method is invoked by the UpdateManager each time a change happens (i.e UpdateInterface)
910  def changed(self, action):
911  # Track unsaved changes
912  self.has_unsaved_changes = True
913 
914  if action.type == "insert":
915  # Insert new item
916  old_vals = self._set(action.key, action.values, add=True)
917  action.set_old_values(old_vals) # Save previous values to reverse this action
918 
919  elif action.type == "update":
920  # Update existing item
921  old_vals = self._set(action.key, action.values, partial_update=action.partial_update)
922  action.set_old_values(old_vals) # Save previous values to reverse this action
923 
924  elif action.type == "delete":
925  # Delete existing item
926  old_vals = self._set(action.key, remove=True)
927  action.set_old_values(old_vals) # Save previous values to reverse this action
928 
929  # Utility methods
930  ##
931  # Generate random alphanumeric ids
932  def generate_id(self, digits=10):
933 
934  chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
935  id = ""
936  for i in range(digits):
937  c_index = random.randint(0, len(chars) - 1)
938  id += (chars[c_index])
939  return id
def needs_save(self)
Returns if project data Has unsaved changes.
Definition: project_data.py:63
def get_app()
Returns the current QApplication instance of OpenShot.
Definition: app.py:54
def generate_id(self, digits=10)
Generate random alphanumeric ids.
def convert_paths_to_absolute(self)
Convert all paths to absolute.
def read_legacy_project_file(self, file_path)
Attempt to read a legacy version 1.x openshot project file.
def load(self, file_path)
Load project from file.
def convert_paths_to_relative(self, file_path)
Convert all paths relative to this filepath.
def _set(self, key, values=None, add=False, partial_update=False, remove=False)
Store setting, but adding isn&#39;t allowed.
def move_temp_paths_to_project_folder(self, file_path)
Move all temp files (such as Thumbnails, Titles, and Blender animations) to the project folder...
def set(self, key, value)
Prevent calling JsonDataStore set() method.
def get_settings()
Get the current QApplication&#39;s settings instance.
Definition: settings.py:43
def add_to_recent_files(self, file_path)
Add this project to the recent files list.
This class allows advanced searching of data structure, implements changes interface.
Definition: project_data.py:45
def new(self)
Try to load default project settings file, will raise error on failure.
def save(self, file_path, move_temp_files=True, make_paths_relative=True)
Save project file to disk.
def changed(self, action)
This method is invoked by the UpdateManager each time a change happens (i.e UpdateInterface) ...
def get(self, key)
Get copied value of a given key in data store.
Definition: project_data.py:68
def upgrade_project_data_structures(self)
Fix any issues with old project files (if any)