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 _set
Store setting, but adding isn't allowed.
def convert_paths_to_relative
Convert all paths relative to this filepath.
def add_to_recent_files
Add this project to the recent files list.
def get_app
Returns the current QApplication instance of OpenShot.
def convert_paths_to_absolute
Convert all paths to absolute.
def load
Load project from file.
def new
Try to load default project settings file, will raise error on failure.
def read_legacy_project_file
Attempt to read a legacy version 1.x openshot project file.
def generate_id
Generate random alphanumeric ids.
def set
Prevent calling JsonDataStore set() method.
def check_if_paths_are_valid
Check if all paths are valid, and prompt to update them if needed.
def get
Get copied value of a given key in data store.
This class allows advanced searching of data structure, implements changes interface.
def move_temp_paths_to_project_folder
Move all temp files (such as Thumbnails, Titles, and Blender animations) to the project folder...
def save
Save project file to disk.
def needs_save
Returns if project data Has unsaved changes.
def upgrade_project_data_structures
Fix any issues with old project files (if any)
def changed
This method is invoked by the UpdateManager each time a change happens (i.e UpdateInterface) ...
def get_settings
Get the current QApplication's settings instance.