OpenShot Video Editor  2.0.0
json_data.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file loads and saves settings (as JSON)
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-2018 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 try:
32  import json
33 except ImportError:
34  import simplejson as json
35 
36 import copy
37 import os
38 import re
39 
40 from classes.logger import log
41 from classes import info
42 
43 # Compiled path regex
44 path_regex = re.compile(r'\"(image|path)\":.*?\"(.*?)\"', re.UNICODE)
45 path_context = {}
46 
47 
48 ##
49 # This class which allows getting/setting of key/value settings, and loading and saving to json files.
50 # Internal storage of a dictionary. Uses json or simplejson packages to serialize and deserialize from json to dictionary.
51 # Keys are assumed to be strings, but subclasses which override get/set methods may use different key types.
52 # The write_to_file and read_from_file methods are key type agnostic.
53 class JsonDataStore:
54 
55  # Create default data storage and default data type for logging messages
56  def __init__(self):
57  self._data = {} # Private data store, accessible through the get and set methods
58  self.data_type = "json data"
59 
60  ##
61  # Get copied value of a given key in data store
62  def get(self, key):
63  key = key.lower()
64 
65  # Determine if the root element is a dictionary or list (i.e. project data or settings data)
66  if type(self._data) == list:
67  # Settings data, search for matching "setting" attribute (i.e. list)
68  # Load user setting's values (for easy merging)
69  user_values = {}
70  for item in self._data:
71  if "setting" in item and "value" in item:
72  user_values[item["setting"].lower()] = item["value"]
73 
74  # Settings data
75  return copy.deepcopy(user_values.get(key, None))
76  else:
77  # Project data (i.e dictionary)
78  return copy.deepcopy(self._data.get(key, None))
79 
80  ##
81  # Store value in key
82  def set(self, key, value):
83  key = key.lower()
84 
85  # Determine if the root element is a dictionary or list (i.e. project data or settings data)
86  if type(self._data) == list:
87  # Settings data, search for matching "setting" attribute (i.e. list)
88  # Load user setting's values (for easy merging)
89  user_values = {}
90  for item in self._data:
91  if "setting" in item and "value" in item:
92  user_values[item["setting"].lower()] = item
93 
94  # Settings data
95  user_values[key]["value"] = value
96 
97  else:
98  # Project data (i.e dictionary)
99  self._data[key] = value
100 
101  ##
102  # Merge settings files, removing invalid settings based on default settings
103  # This is only called by some sub-classes that use string keys
104  def merge_settings(self, default, user):
105 
106  # Determine if the root element is a dictionary or list (i.e. project data or settings data)
107  if type(default) == list:
108 
109  # Load user setting's values (for easy merging)
110  user_values = {}
111  for item in user:
112  if "setting" in item and "value" in item:
113  user_values[item["setting"]] = item["value"]
114 
115  # Update default values to match user values
116  for item in default:
117  user_value = user_values.get(item["setting"], None)
118  if user_value != None:
119  item["value"] = user_value
120 
121  # Return merged list
122  return default
123 
124  else:
125  # Root object is a dictionary (i.e. project data)
126  for key in default:
127  if key not in user:
128  # Add missing key to user dictionary
129  user[key] = default[key]
130 
131  # Return merged dictionary
132  return user
133 
134  ##
135  # Load JSON settings from a file
136  def read_from_file(self, file_path, path_mode="ignore"):
137  try:
138  with open(file_path, 'r') as f:
139  contents = f.read()
140  if contents:
141  if path_mode == "absolute":
142  # Convert any paths to absolute
143  contents = self.convert_paths_to_absolute(file_path, contents)
144  return json.loads(contents, strict=False)
145  except Exception as ex:
146  msg = ("Couldn't load {} file: {}".format(self.data_type, ex))
147  log.error(msg)
148  raise Exception(msg)
149  msg = ("Couldn't load {} file, no data.".format(self.data_type))
150  log.warning(msg)
151  raise Exception(msg)
152 
153  ##
154  # Save JSON settings to a file
155  def write_to_file(self, file_path, data, path_mode="ignore", previous_path=None):
156  try:
157  contents = json.dumps(data, indent=4)
158  if path_mode == "relative":
159  # Convert any paths to relative
160  contents = self.convert_paths_to_relative(file_path, previous_path, contents)
161  with open(file_path, 'w') as f:
162  f.write(contents)
163  except Exception as ex:
164  msg = ("Couldn't save {} file:\n{}\n{}".format(self.data_type, file_path, ex))
165  log.error(msg)
166  raise Exception(msg)
167 
168  ##
169  # Replace matched string for converting paths to relative paths
170  def replace_string_to_absolute(self, match):
171  key = match.groups(0)[0]
172  path = match.groups(0)[1]
173 
174  # Find absolute path of file (if needed)
175  utf_path = json.loads('"%s"' % path, encoding="utf-8") # parse bytestring into unicode string
176  if "@transitions" not in utf_path:
177  # Convert path to the correct relative path (based on the existing folder)
178  new_path = os.path.abspath(os.path.join(path_context.get("existing_project_folder", ""), utf_path))
179  new_path = json.dumps(new_path) # Escape backslashes
180  return '"%s": %s' % (key, new_path)
181 
182  # Determine if @transitions path is found
183  else:
184  new_path = path.replace("@transitions", os.path.join(info.PATH, "transitions"))
185  new_path = json.dumps(new_path) # Escape backslashes
186  return '"%s": %s' % (key, new_path)
187 
188  ##
189  # Convert all paths to absolute using regex
190  def convert_paths_to_absolute(self, file_path, data):
191  try:
192  # Get project folder
193  path_context["new_project_folder"] = os.path.dirname(file_path)
194  path_context["existing_project_folder"] = os.path.dirname(file_path)
195 
196  # Optimized regex replacement
197  data = re.sub(path_regex, self.replace_string_to_absolute, data)
198 
199  except Exception as ex:
200  log.error("Error while converting relative paths to absolute paths: %s" % str(ex))
201 
202  return data
203 
204  ##
205  # Replace matched string for converting paths to relative paths
206  def replace_string_to_relative(self, match):
207  key = match.groups(0)[0]
208  path = match.groups(0)[1]
209  utf_path = json.loads('"%s"' % path, encoding="utf-8") # parse bytestring into unicode string
210  folder_path, file_path = os.path.split(os.path.abspath(utf_path))
211 
212  # Determine if thumbnail path is found
213  if info.THUMBNAIL_PATH in folder_path:
214  # Convert path to relative thumbnail path
215  new_path = os.path.join("thumbnail", file_path).replace("\\", "/")
216  new_path = json.dumps(new_path) # Escape backslashes
217  return '"%s": %s' % (key, new_path)
218 
219  # Determine if @transitions path is found
220  elif os.path.join(info.PATH, "transitions") in folder_path:
221  # Yes, this is an OpenShot transitions
222  folder_path, category_path = os.path.split(folder_path)
223 
224  # Convert path to @transitions/ path
225  new_path = os.path.join("@transitions", category_path, file_path).replace("\\", "/")
226  new_path = json.dumps(new_path) # Escape backslashes
227  return '"%s": %s' % (key, new_path)
228 
229  # Find absolute path of file (if needed)
230  else:
231  # Convert path to the correct relative path (based on the existing folder)
232  orig_abs_path = os.path.abspath(utf_path)
233 
234  # Remove file from abs path
235  orig_abs_folder = os.path.split(orig_abs_path)[0]
236 
237  # Calculate new relateive path
238  new_rel_path_folder = os.path.relpath(orig_abs_folder, path_context.get("new_project_folder", ""))
239  new_rel_path = os.path.join(new_rel_path_folder, file_path).replace("\\", "/")
240  new_rel_path = json.dumps(new_rel_path) # Escape backslashes
241  return '"%s": %s' % (key, new_rel_path)
242 
243  ##
244  # Convert all paths relative to this filepath
245  def convert_paths_to_relative(self, file_path, previous_path, data):
246  try:
247  # Get project folder
248  path_context["new_project_folder"] = os.path.dirname(file_path)
249  path_context["existing_project_folder"] = os.path.dirname(file_path)
250  if previous_path:
251  path_context["existing_project_folder"] = os.path.dirname(previous_path)
252 
253  # Optimized regex replacement
254  data = re.sub(path_regex, self.replace_string_to_relative, data)
255 
256  except Exception as ex:
257  log.error("Error while converting absolute paths to relative paths: %s" % str(ex))
258 
259  return data