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
48 JsonDataStore.__init__(self)
71 if not isinstance(key, list):
72 log.warning(
"get() key must be a list. key: {}".format(key))
75 log.warning(
"Cannot get empty key.")
82 for key_index
in range(len(key)):
83 key_part = key[key_index]
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__))
92 if isinstance(key_part, dict)
and isinstance(obj, list):
96 for item_index
in range(len(obj)):
97 item = obj[item_index]
101 for subkey
in key_part.keys():
103 subkey = subkey.lower()
105 if not (subkey
in item
and item[subkey] == key_part[subkey]):
118 if isinstance(key_part, str):
119 key_part = key_part.lower()
122 if not isinstance(obj, dict):
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))
129 if not key_part
in obj:
130 log.warn(
"Key not found in project. Mismatch on key part {} (\"{}\").\nKey: {}".format((key_index),
143 def set(self, key, value):
144 raise Exception(
"ProjectDataStore.set() is not allowed. Changes must route through UpdateManager.")
148 def _set(self, key, values=None, add=False, partial_update=False, remove=False):
151 "_set key: {} values: {} add: {} partial: {} remove: {}".format(key, values, add, partial_update, remove))
152 parent, my_key =
None,
"" 155 if not isinstance(key, list):
156 log.warning(
"_set() key must be a list. key: {}".format(key))
159 log.warning(
"Cannot set empty key.")
166 for key_index
in range(len(key)):
167 key_part = key[key_index]
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__))
176 if isinstance(key_part, dict)
and isinstance(obj, list):
180 for item_index
in range(len(obj)):
181 item = obj[item_index]
185 for subkey
in key_part.keys():
187 subkey = subkey.lower()
189 if not (subkey
in item
and item[subkey] == key_part[subkey]):
204 if isinstance(key_part, str):
205 key_part = key_part.lower()
208 if not isinstance(obj, dict):
212 if not key_part
in obj:
213 log.warn(
"Key not found in project. Mismatch on key part {} (\"{}\").\nKey: {}".format((key_index),
224 if key_index < (len(key) - 1)
or key_index == 0:
229 ret = copy.deepcopy(obj)
239 if add
and isinstance(parent, list):
241 parent.append(values)
244 elif isinstance(values, dict):
251 self.
_data[my_key] = values
267 default_profile = s.get(
"default-profile")
270 for profile_folder
in [info.USER_PROFILES_PATH, info.PROFILES_PATH]:
271 for file
in os.listdir(profile_folder):
273 profile_path = os.path.join(profile_folder, file)
274 profile = openshot.Profile(profile_path)
276 if default_profile == profile.info.description:
277 log.info(
"Setting default profile to %s" % profile.info.description)
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}
287 default_sample_rate = int(s.get(
"default-samplerate"))
288 default_channel_ayout = s.get(
"default-channellayout")
291 channel_layout = openshot.LAYOUT_STEREO
292 if default_channel_ayout ==
"LAYOUT_MONO":
294 channel_layout = openshot.LAYOUT_MONO
295 elif default_channel_ayout ==
"LAYOUT_STEREO":
297 channel_layout = openshot.LAYOUT_STEREO
298 elif default_channel_ayout ==
"LAYOUT_SURROUND":
300 channel_layout = openshot.LAYOUT_SURROUND
301 elif default_channel_ayout ==
"LAYOUT_5POINT1":
303 channel_layout = openshot.LAYOUT_5POINT1
304 elif default_channel_ayout ==
"LAYOUT_7POINT1":
306 channel_layout = openshot.LAYOUT_7POINT1
309 self.
_data[
"sample_rate"] = default_sample_rate
310 self.
_data[
"channels"] = channels
311 self.
_data[
"channel_layout"] = channel_layout
321 default_project = self.
_data 325 project_data = self.read_from_file(file_path)
327 except Exception
as ex:
332 except Exception
as ex:
337 self.
_data = self.merge_settings(default_project, project_data)
350 project_thumbnails_folder = os.path.join(loaded_project_folder,
"thumbnail")
351 if os.path.exists(project_thumbnails_folder):
353 shutil.rmtree(info.THUMBNAIL_PATH,
True)
356 shutil.copytree(project_thumbnails_folder, info.THUMBNAIL_PATH)
365 from classes.app
import get_app
375 from classes.query
import File, Track, Clip, Transition
376 from classes.app
import get_app
382 import simplejson
as json
388 v = openshot.GetVersion()
390 project_data[
"version"] = {
"openshot-qt" : info.VERSION,
391 "libopenshot" : v.ToString()}
394 from classes.app
import get_app
395 fps =
get_app().project.get([
"fps"])
396 fps_float = float(fps[
"num"]) / float(fps[
"den"])
399 from classes.legacy.openshot
import classes
as legacy_classes
400 from classes.legacy.openshot.classes
import project
as legacy_project
401 from classes.legacy.openshot.classes
import sequences
as legacy_sequences
402 from classes.legacy.openshot.classes
import track
as legacy_track
403 from classes.legacy.openshot.classes
import clip
as legacy_clip
404 from classes.legacy.openshot.classes
import keyframe
as legacy_keyframe
405 from classes.legacy.openshot.classes
import files
as legacy_files
406 from classes.legacy.openshot.classes
import transition
as legacy_transition
407 from classes.legacy.openshot.classes
import effect
as legacy_effect
408 from classes.legacy.openshot.classes
import marker
as legacy_marker
409 sys.modules[
'openshot.classes'] = legacy_classes
410 sys.modules[
'classes.project'] = legacy_project
411 sys.modules[
'classes.sequences'] = legacy_sequences
412 sys.modules[
'classes.track'] = legacy_track
413 sys.modules[
'classes.clip'] = legacy_clip
414 sys.modules[
'classes.keyframe'] = legacy_keyframe
415 sys.modules[
'classes.files'] = legacy_files
416 sys.modules[
'classes.transition'] = legacy_transition
417 sys.modules[
'classes.effect'] = legacy_effect
418 sys.modules[
'classes.marker'] = legacy_marker
423 with open(file_path.encode(
'UTF-8'),
'rb')
as f:
426 v1_data = pickle.load(f, fix_imports=
True, encoding=
"UTF-8")
430 for item
in v1_data.project_folder.items:
432 if isinstance(item, legacy_files.OpenShotFile):
435 clip = openshot.Clip(item.name)
436 reader = clip.Reader()
437 file_data = json.loads(reader.Json())
440 if file_data[
"has_video"]
and not self.
is_image(file_data):
441 file_data[
"media_type"] =
"video" 442 elif file_data[
"has_video"]
and self.
is_image(file_data):
443 file_data[
"media_type"] =
"image" 444 elif file_data[
"has_audio"]
and not file_data[
"has_video"]:
445 file_data[
"media_type"] =
"audio" 449 file.data = file_data
453 file_lookup[item.unique_id] = file
457 msg = (
"%s is not a valid video, audio, or image file." % item.name)
459 failed_files.append(item.name)
462 track_list = copy.deepcopy(Track.filter())
463 for track
in track_list:
468 for legacy_t
in reversed(v1_data.sequences[0].tracks):
470 t.data = {
"number": track_counter,
"y": 0,
"label": legacy_t.name}
477 for sequence
in v1_data.sequences:
478 for track
in reversed(sequence.tracks):
479 for clip
in track.clips:
481 if clip.file_object.unique_id
in file_lookup.keys():
482 file = file_lookup[clip.file_object.unique_id]
485 log.info(
"Skipping importing missing file: %s" % clip.file_object.unique_id)
489 if (file.data[
"media_type"] ==
"video" or file.data[
"media_type"] ==
"image"):
491 thumb_path = os.path.join(info.THUMBNAIL_PATH,
"%s.png" % file.data[
"id"])
494 thumb_path = os.path.join(info.PATH,
"images",
"AudioThumbnail.png")
497 path, filename = os.path.split(file.data[
"path"])
500 file_path = file.absolute_path()
503 c = openshot.Clip(file_path)
506 new_clip = json.loads(c.Json())
507 new_clip[
"file_id"] = file.id
508 new_clip[
"title"] = filename
509 new_clip[
"image"] = thumb_path
512 new_clip[
"start"] = clip.start_time
513 new_clip[
"end"] = clip.end_time
514 new_clip[
"position"] = clip.position_on_track
515 new_clip[
"layer"] = track_counter
518 if clip.video_fade_in
or clip.video_fade_out:
519 new_clip[
"alpha"][
"Points"] = []
522 if clip.video_fade_in:
524 start = openshot.Point(round(clip.start_time * fps_float) + 1, 0.0, openshot.BEZIER)
525 start_object = json.loads(start.Json())
526 end = openshot.Point(round((clip.start_time + clip.video_fade_in_amount) * fps_float) + 1, 1.0, openshot.BEZIER)
527 end_object = json.loads(end.Json())
528 new_clip[
"alpha"][
"Points"].append(start_object)
529 new_clip[
"alpha"][
"Points"].append(end_object)
532 if clip.video_fade_out:
534 start = openshot.Point(round((clip.end_time - clip.video_fade_out_amount) * fps_float) + 1, 1.0, openshot.BEZIER)
535 start_object = json.loads(start.Json())
536 end = openshot.Point(round(clip.end_time * fps_float) + 1, 0.0, openshot.BEZIER)
537 end_object = json.loads(end.Json())
538 new_clip[
"alpha"][
"Points"].append(start_object)
539 new_clip[
"alpha"][
"Points"].append(end_object)
542 if clip.audio_fade_in
or clip.audio_fade_out:
543 new_clip[
"volume"][
"Points"] = []
545 p = openshot.Point(1, clip.volume / 100.0, openshot.BEZIER)
546 p_object = json.loads(p.Json())
547 new_clip[
"volume"] = {
"Points" : [p_object]}
550 if clip.audio_fade_in:
552 start = openshot.Point(round(clip.start_time * fps_float) + 1, 0.0, openshot.BEZIER)
553 start_object = json.loads(start.Json())
554 end = openshot.Point(round((clip.start_time + clip.video_fade_in_amount) * fps_float) + 1, clip.volume / 100.0, openshot.BEZIER)
555 end_object = json.loads(end.Json())
556 new_clip[
"volume"][
"Points"].append(start_object)
557 new_clip[
"volume"][
"Points"].append(end_object)
560 if clip.audio_fade_out:
562 start = openshot.Point(round((clip.end_time - clip.video_fade_out_amount) * fps_float) + 1, clip.volume / 100.0, openshot.BEZIER)
563 start_object = json.loads(start.Json())
564 end = openshot.Point(round(clip.end_time * fps_float) + 1, 0.0, openshot.BEZIER)
565 end_object = json.loads(end.Json())
566 new_clip[
"volume"][
"Points"].append(start_object)
567 new_clip[
"volume"][
"Points"].append(end_object)
571 clip_object.data = new_clip
575 for trans
in track.transitions:
577 if not trans.resource
or not os.path.exists(trans.resource):
578 trans.resource = os.path.join(info.PATH,
"transitions",
"common",
"fade.svg")
581 transition_reader = openshot.QtImageReader(trans.resource)
583 trans_begin_value = 1.0
584 trans_end_value = -1.0
586 trans_begin_value = -1.0
587 trans_end_value = 1.0
589 brightness = openshot.Keyframe()
590 brightness.AddPoint(1, trans_begin_value, openshot.BEZIER)
591 brightness.AddPoint(round(trans.length * fps_float) + 1, trans_end_value, openshot.BEZIER)
592 contrast = openshot.Keyframe(trans.softness * 10.0)
596 "id":
get_app().project.generate_id(),
597 "layer": track_counter,
598 "title":
"Transition",
600 "position": trans.position_on_track,
603 "brightness": json.loads(brightness.Json()),
604 "contrast": json.loads(contrast.Json()),
605 "reader": json.loads(transition_reader.Json()),
606 "replace_image":
False 611 t.data = transitions_data
617 except Exception
as ex:
619 msg = _(
"Failed to load project file %(path)s: %(error)s" % {
"path": file_path,
"error": ex})
626 raise Exception(_(
"Failed to load the following files:\n%s" %
", ".join(failed_files)))
629 log.info(
"Successfully loaded legacy project file: %s" % file_path)
633 path = file[
"path"].lower()
635 if path.endswith((
".jpg",
".jpeg",
".png",
".bmp",
".svg",
".thm",
".gif",
".bmp",
".pgm",
".tif",
".tiff")):
643 openshot_version = self.
_data[
"version"][
"openshot-qt"]
644 libopenshot_version = self.
_data[
"version"][
"libopenshot"]
646 log.info(openshot_version)
647 log.info(libopenshot_version)
649 if openshot_version ==
"0.0.0":
652 for clip
in self.
_data[
"clips"]:
654 for point
in clip[
"alpha"][
"Points"]:
657 point[
"co"][
"Y"] = 1.0 - point[
"co"][
"Y"]
658 if "handle_left" in point:
659 point[
"handle_left"][
"Y"] = 1.0 - point[
"handle_left"][
"Y"]
660 if "handle_right" in point:
661 point[
"handle_right"][
"Y"] = 1.0 - point[
"handle_right"][
"Y"]
663 elif openshot_version <=
"2.1.0-dev":
666 for clip_type
in [
"clips",
"effects"]:
667 for clip
in self.
_data[clip_type]:
668 for object
in [clip] + clip.get(
'effects',[]):
669 for item_key, item_data
in object.items():
671 if type(item_data) == dict
and "Points" in item_data:
672 for point
in item_data.get(
"Points"):
674 if "handle_left" in point:
676 point.get(
"handle_left")[
"X"] = 0.5
677 point.get(
"handle_left")[
"Y"] = 1.0
678 if "handle_right" in point:
680 point.get(
"handle_right")[
"X"] = 0.5
681 point.get(
"handle_right")[
"Y"] = 0.0
683 elif type(item_data) == dict
and "red" in item_data:
684 for color
in [
"red",
"blue",
"green",
"alpha"]:
685 for point
in item_data.get(color).
get(
"Points"):
687 if "handle_left" in point:
689 point.get(
"handle_left")[
"X"] = 0.5
690 point.get(
"handle_left")[
"Y"] = 1.0
691 if "handle_right" in point:
693 point.get(
"handle_right")[
"X"] = 0.5
694 point.get(
"handle_right")[
"Y"] = 0.0
698 def save(self, file_path, move_temp_files=True, make_paths_relative=True):
706 if make_paths_relative:
710 v = openshot.GetVersion()
711 self.
_data[
"version"] = {
"openshot-qt" : info.VERSION,
712 "libopenshot" : v.ToString() }
715 self.write_to_file(file_path, self.
_data)
721 if make_paths_relative:
735 new_project_folder = os.path.dirname(file_path)
736 new_thumbnails_folder = os.path.join(new_project_folder,
"thumbnail")
739 if not os.path.exists(new_thumbnails_folder):
740 os.mkdir(new_thumbnails_folder)
743 for filename
in glob.glob(os.path.join(info.THUMBNAIL_PATH,
'*.*')):
744 shutil.copy(filename, new_thumbnails_folder)
747 for file
in self.
_data[
"files"]:
751 if info.BLENDER_PATH
in path
or info.ASSETS_PATH
in path:
752 log.info(
"Temp blender file path detected in file")
755 folder_path, file_name = os.path.split(path)
756 parent_path, folder_name = os.path.split(folder_path)
757 new_parent_path = new_project_folder
759 if os.path.isdir(path)
or "%" in path:
761 new_parent_path = os.path.join(new_project_folder, folder_name)
764 shutil.copytree(folder_path, new_parent_path)
767 new_parent_path = os.path.join(new_project_folder,
"assets")
770 if not os.path.exists(new_parent_path):
771 os.mkdir(new_parent_path)
774 shutil.copy2(path, os.path.join(new_parent_path, file_name))
777 file[
"path"] = os.path.join(new_parent_path, file_name)
780 for clip
in self.
_data[
"clips"]:
781 path = clip[
"reader"][
"path"]
784 if info.BLENDER_PATH
in path
or info.ASSETS_PATH
in path:
785 log.info(
"Temp blender file path detected in clip")
788 folder_path, file_name = os.path.split(path)
789 parent_path, folder_name = os.path.split(folder_path)
791 path = os.path.join(new_project_folder, folder_name)
794 clip[
"reader"][
"path"] = os.path.join(path, file_name)
797 for clip
in self.
_data[
"clips"]:
801 if info.BLENDER_PATH
in path
or info.ASSETS_PATH
in path:
802 log.info(
"Temp blender file path detected in clip thumbnail")
805 folder_path, file_name = os.path.split(path)
806 parent_path, folder_name = os.path.split(folder_path)
808 path = os.path.join(new_project_folder, folder_name)
811 clip[
"image"] = os.path.join(path, file_name)
813 except Exception
as ex:
814 log.error(
"Error while moving temp files into project folder: %s" % str(ex))
819 if "backup.osp" in file_path:
824 recent_projects = s.get(
"recent_projects")
827 if file_path
in recent_projects:
828 recent_projects.remove(file_path)
831 if len(recent_projects) > 10:
832 del recent_projects[0]
835 recent_projects.append(file_path)
838 s.set(
"recent_projects", recent_projects)
846 existing_project_folder =
None 849 new_project_folder = os.path.dirname(file_path)
852 for file
in self.
_data[
"files"]:
855 if not os.path.isabs(path):
857 path = os.path.abspath(os.path.join(existing_project_folder, path))
860 file[
"path"] = os.path.relpath(path, new_project_folder)
863 for clip
in self.
_data[
"clips"]:
865 path = clip[
"reader"][
"path"]
867 if not os.path.isabs(path):
869 path = os.path.abspath(os.path.join(existing_project_folder, path))
871 clip[
"reader"][
"path"] = os.path.relpath(path, new_project_folder)
876 if not os.path.isabs(path):
878 path = os.path.abspath(os.path.join(existing_project_folder, path))
880 clip[
"image"] = os.path.relpath(path, new_project_folder)
883 for effect
in self.
_data[
"effects"]:
885 path = effect[
"reader"][
"path"]
888 folder_path, file_path = os.path.split(path)
889 if os.path.join(info.PATH,
"transitions")
in folder_path:
891 folder_path, category_path = os.path.split(folder_path)
894 effect[
"reader"][
"path"] = os.path.join(
"@transitions", category_path, file_path)
898 if not os.path.isabs(path):
900 path = os.path.abspath(os.path.join(existing_project_folder, path))
902 effect[
"reader"][
"path"] = os.path.relpath(path, new_project_folder)
904 except Exception
as ex:
905 log.error(
"Error while converting absolute paths to relative paths: %s" % str(ex))
912 starting_folder =
None 913 if self.
_data[
"import_path"]:
914 starting_folder = os.path.join(self.
_data[
"import_path"])
919 from classes.app
import get_app
925 for file
in reversed(self.
_data[
"files"]):
927 parent_path, file_name_with_ext = os.path.split(path)
928 while not os.path.exists(path)
and "%" not in path:
930 QMessageBox.warning(
None, _(
"Missing File (%s)") % file[
"id"], _(
"%s cannot be found.") % file_name_with_ext)
931 starting_folder = QFileDialog.getExistingDirectory(
None, _(
"Find directory that contains: %s" % file_name_with_ext), starting_folder)
932 log.info(
"Missing folder chosen by user: %s" % starting_folder)
935 path = os.path.join(starting_folder, file_name_with_ext)
937 get_app().updates.update([
"import_path"], os.path.dirname(path))
939 log.info(
'Removed missing file: %s' % file_name_with_ext)
940 self.
_data[
"files"].remove(file)
944 for clip
in reversed(self.
_data[
"clips"]):
945 path = clip[
"reader"][
"path"]
946 parent_path, file_name_with_ext = os.path.split(path)
947 while not os.path.exists(path)
and "%" not in path:
949 QMessageBox.warning(
None, _(
"Missing File in Clip (%s)") % clip[
"id"], _(
"%s cannot be found.") % file_name_with_ext)
950 starting_folder = QFileDialog.getExistingDirectory(
None, _(
"Find directory that contains: %s" % file_name_with_ext), starting_folder)
951 log.info(
"Missing folder chosen by user: %s" % starting_folder)
954 path = os.path.join(starting_folder, file_name_with_ext)
955 clip[
"reader"][
"path"] = path
957 log.info(
'Removed missing clip: %s' % file_name_with_ext)
958 self.
_data[
"clips"].remove(clip)
966 existing_project_folder =
None 971 for file
in self.
_data[
"files"]:
974 if not os.path.isabs(path):
976 path = os.path.abspath(os.path.join(existing_project_folder, path))
982 for clip
in self.
_data[
"clips"]:
984 path = clip[
"reader"][
"path"]
986 if not os.path.isabs(path):
988 path = os.path.abspath(os.path.join(existing_project_folder, path))
990 clip[
"reader"][
"path"] = path
995 if not os.path.isabs(path):
997 path = os.path.abspath(os.path.join(existing_project_folder, path))
1002 for effect
in self.
_data[
"effects"]:
1004 path = effect[
"reader"][
"path"]
1007 if "@transitions" in path:
1008 path = path.replace(
"@transitions", os.path.join(info.PATH,
"transitions"))
1011 if not os.path.isabs(path):
1013 path = os.path.abspath(os.path.join(existing_project_folder, path))
1015 effect[
"reader"][
"path"] = path
1017 except Exception
as ex:
1018 log.error(
"Error while converting relative paths to absolute paths: %s" % str(ex))
1026 if action.type ==
"insert":
1028 old_vals = self.
_set(action.key, action.values, add=
True)
1029 action.set_old_values(old_vals)
1031 elif action.type ==
"update":
1033 old_vals = self.
_set(action.key, action.values, partial_update=action.partial_update)
1034 action.set_old_values(old_vals)
1036 elif action.type ==
"delete":
1038 old_vals = self.
_set(action.key, remove=
True)
1039 action.set_old_values(old_vals)
1046 chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 1048 for i
in range(digits):
1049 c_index = random.randint(0, len(chars) - 1)
1050 id += (chars[c_index])
def needs_save(self)
Returns if project data Has unsaved changes.
def get_app()
Returns the current QApplication instance of OpenShot.
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'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's settings instance.
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.
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.
def check_if_paths_are_valid(self)
Check if all paths are valid, and prompt to update them if needed.
def upgrade_project_data_structures(self)
Fix any issues with old project files (if any)