Skip to content

Commit 944623c

Browse files
committed
- Add 'end' property correctly when using AddToTimeline dialog (for split clips)
- Large refactor to File Properties dialog (update related clips after changing file path or FPS, update form after switching files, scale start/end when changing FPS, group transactions for undo/redo, update thumbnails for all related clips and file) - Adding Clips refactored to re-use File object data (instead of a fresh Clip Reader) - faster and more sane. Refactor thumbnail updating. Also, respect 'start' and 'end' properties of File object.
1 parent 2d0c22f commit 944623c

File tree

6 files changed

+102
-57
lines changed

6 files changed

+102
-57
lines changed

src/classes/thumbnail.py

+25
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import openshot
3131
import socket
3232
import time
33+
from requests import get
3334
from threading import Thread
3435
from classes import info
3536
from classes.query import File
@@ -47,6 +48,30 @@
4748
REGEX_THUMBNAIL_URL = re.compile(r"/thumbnails/(?P<file_id>.+?)/(?P<file_frame>\d+)/*(?P<only_path>path)?/*(?P<no_cache>no-cache)?")
4849

4950

51+
def GetThumbPath(file_id, thumbnail_frame, clear_cache=False):
52+
"""Get thumbnail path by invoking HTTP thumbnail request"""
53+
54+
# Clear thumb cache (if requested)
55+
thumb_cache = ""
56+
if clear_cache:
57+
thumb_cache = "no-cache/"
58+
59+
# Connect to thumbnail server and get image
60+
thumb_server_details = get_app().window.http_server_thread.server_address
61+
thumb_address = "http://%s:%s/thumbnails/%s/%s/path/%s" % (
62+
thumb_server_details[0],
63+
thumb_server_details[1],
64+
file_id,
65+
thumbnail_frame,
66+
thumb_cache)
67+
r = get(thumb_address)
68+
if r.ok:
69+
# Update thumbnail path to real one
70+
return r.text
71+
else:
72+
return ''
73+
74+
5075
def GenerateThumbnail(file_path, thumb_path, thumbnail_frame, width, height, mask, overlay):
5176
"""Create thumbnail image, and check for rotate metadata (if any)"""
5277

src/windows/add_to_timeline.py

+2
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ def accept(self):
217217
if file.data["media_type"] == "image":
218218
end_time = image_length
219219
new_clip["end"] = end_time
220+
else:
221+
new_clip["end"] = end_time
220222

221223
# Adjust Fade of Clips (if no transition is chosen)
222224
if not transition_path:

src/windows/file_properties.py

+58-9
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@
3535
# Python module for libopenshot (required video editing module installed separately)
3636
import openshot
3737

38+
from uuid import uuid4
3839
from classes import info, ui_util
3940
from classes.app import get_app
4041
from classes.image_types import get_media_type
4142
from classes.logger import log
4243
from classes.metrics import track_metric_screen
44+
from classes.query import Clip
4345

4446

4547
class FileProperties(QDialog):
@@ -78,6 +80,16 @@ def __init__(self, file):
7880
# Dynamically load tabs from settings data
7981
self.settings_data = self.s.get_all_settings()
8082

83+
# Initialize Form
84+
self.channel_layout_choices = []
85+
self.initialize()
86+
87+
def initialize(self):
88+
"""Init all form elements / textboxes / etc..."""
89+
# get translations
90+
app = get_app()
91+
_ = app._tr
92+
8193
# Get file properties
8294
filename = os.path.basename(self.file.data["path"])
8395
file_extension = os.path.splitext(filename)[1]
@@ -119,7 +131,7 @@ def __init__(self, file):
119131
self.txtAudioBitRate.setValue(int(self.file.data["audio_bit_rate"]))
120132

121133
# Populate output field
122-
self.txtOutput.setText(json.dumps(file.data, sort_keys=True, indent=2))
134+
self.txtOutput.setText(json.dumps(self.file.data, sort_keys=True, indent=2))
123135

124136
# Add channel layouts
125137
selected_channel_layout_index = 0
@@ -213,6 +225,9 @@ def browsePath(self):
213225
# verify path is valid (and prompt for image sequence if detected)
214226
self.verifyPath(new_path)
215227

228+
# re-init form
229+
self.initialize()
230+
216231
def accept(self):
217232
new_path = self.txtFilePath.text()
218233
if new_path and self.file.data.get("path") != new_path:
@@ -222,23 +237,57 @@ def accept(self):
222237
# Update file details
223238
self.file.data["name"] = self.txtFileName.text()
224239
self.file.data["tags"] = self.txtTags.text()
225-
226-
# Update Framerate
227-
self.file.data["fps"]["num"] = self.txtFrameRateNum.value()
228-
self.file.data["fps"]["den"] = self.txtFrameRateDen.value()
229-
230-
# Update start / end frame
231-
fps_float = float(self.file.data["fps"]["num"]) / float(self.file.data["fps"]["den"])
232-
if self.txtStartFrame.value() != 1 or self.txtEndFrame.value() != int(self.file.data["video_length"]):
240+
241+
# Determine if FPS changed
242+
fps_float = self.txtFrameRateNum.value() / self.txtFrameRateDen.value()
243+
if self.file.data["fps"]["num"] != self.txtFrameRateNum.value() or \
244+
self.file.data["fps"]["den"] != self.txtFrameRateDen.value():
245+
original_fps_float = float(self.file.data["fps"]["num"]) / float(self.file.data["fps"]["den"])
246+
# Update file 'fps' and 'video_timebase'
247+
self.file.data["fps"]["num"] = self.txtFrameRateNum.value()
248+
self.file.data["fps"]["den"] = self.txtFrameRateDen.value()
249+
self.file.data["video_timebase"]["num"] = self.txtFrameRateDen.value()
250+
self.file.data["video_timebase"]["den"] = self.txtFrameRateNum.value()
251+
252+
# Scale 'start' and 'end' properties by FPS difference
253+
fps_diff = original_fps_float / fps_float
254+
self.file.data["duration"] *= fps_diff
255+
if "start" in self.file.data:
256+
self.file.data["start"] *= fps_diff
257+
if "end" in self.file.data:
258+
self.file.data["end"] *= fps_diff
259+
260+
# Scale 'start' and 'end' file attributes (if changed)
261+
elif self.txtStartFrame.value() != 1 or self.txtEndFrame.value() != int(self.file.data["video_length"]):
262+
# Scale 'start' and 'end' properties by FPS difference
233263
self.file.data["start"] = (self.txtStartFrame.value() - 1) / fps_float
234264
self.file.data["end"] = (self.txtEndFrame.value() - 1) / fps_float
235265

266+
# Transaction id to group all updates together
267+
tid = str(uuid4())
268+
get_app().updates.transaction_id = tid
269+
236270
# Save file object
237271
self.file.save()
238272

239273
# Update file info & thumbnail
240274
get_app().window.FileUpdated.emit(self.file.id)
241275

276+
# Update related clips
277+
for clip in Clip.filter(file_id=self.file.id):
278+
clip.data["reader"] = self.file.data
279+
clip.data["duration"] = self.file.data["duration"]
280+
if clip.data["end"] > clip.data["duration"]:
281+
clip.data["end"] = clip.data["duration"]
282+
clip.save()
283+
284+
# Emit thumbnail update signal (to update timeline thumb image)
285+
thumbnail_frame = (clip.data["start"] * fps_float) + 1
286+
get_app().window.ThumbnailUpdated.emit(clip.id, thumbnail_frame)
287+
288+
# Done grouping transactions
289+
get_app().updates.transaction_id = None
290+
242291
# Accept dialog
243292
super(FileProperties, self).accept()
244293

src/windows/main_window.py

+1-13
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class MainWindow(updates.UpdateWatcher, QMainWindow):
106106
MaxSizeChanged = pyqtSignal(object)
107107
InsertKeyframe = pyqtSignal(object)
108108
OpenProjectSignal = pyqtSignal(str)
109-
ThumbnailUpdated = pyqtSignal(str)
109+
ThumbnailUpdated = pyqtSignal(str, int)
110110
FileUpdated = pyqtSignal(str)
111111
CaptionTextUpdated = pyqtSignal(str, object)
112112
CaptionTextLoaded = pyqtSignal(str, object)
@@ -2162,18 +2162,6 @@ def actionFile_Properties_trigger(self):
21622162
# Run the dialog event loop - blocking interaction on this window during that time
21632163
result = win.exec_()
21642164
if result == QDialog.Accepted:
2165-
2166-
# BRUTE FORCE approach: go through all clips and update file data
2167-
clips = Clip.filter(file_id=f.id)
2168-
for c in clips:
2169-
# update clip
2170-
c.data["reader"] = f.data
2171-
c.data["duration"] = f.data["duration"]
2172-
c.save()
2173-
2174-
# Emit thumbnail update signal (to update timeline thumb image)
2175-
self.ThumbnailUpdated.emit(c.id)
2176-
21772165
log.info('File Properties Finished')
21782166
else:
21792167
log.info('File Properties Cancelled')

src/windows/models/files_model.py

+4-32
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
from classes.query import File
4646
from classes.logger import log
4747
from classes.app import get_app
48-
from requests import get
48+
from classes.thumbnail import GetThumbPath
4949

5050
import openshot
5151

@@ -203,7 +203,7 @@ def update_model(self, clear=True, delete_file_id=None, update_file_id=None):
203203
thumbnail_frame = round(float(file.data['start']) * fps_float) + 1
204204

205205
# Get thumb path
206-
thumb_icon = QIcon(self.get_thumb_path(file.id, thumbnail_frame))
206+
thumb_icon = QIcon(GetThumbPath(file.id, thumbnail_frame))
207207
else:
208208
# Audio file
209209
thumb_icon = QIcon(os.path.join(info.PATH, "images", "AudioThumbnail.svg"))
@@ -314,17 +314,13 @@ def add_files(self, files, image_seq_details=None, quiet=False,
314314
if seq_info:
315315
# Update file with image sequence path & name
316316
new_path = seq_info.get("path")
317-
new_file.data["name"] = os.path.basename(new_path)
318317

319318
# Load image sequence (to determine duration and video_length)
320319
clip = openshot.Clip(new_path)
320+
new_file.data = json.loads(clip.Reader().Json())
321321
if clip and clip.info.duration > 0.0:
322-
323322
# Update file details
324-
new_file.data["path"] = new_path
325323
new_file.data["media_type"] = "video"
326-
new_file.data["duration"] = clip.Reader().info.duration
327-
new_file.data["video_length"] = "%s" % clip.Reader().info.video_length
328324

329325
if seq_info and "fps" in seq_info:
330326
# Blender Titles specify their fps in seq_info
@@ -489,30 +485,6 @@ def process_urls(self, qurl_list):
489485
log.debug("Importing file list: {}".format(media_paths))
490486
self.add_files(media_paths, quiet=import_quietly)
491487

492-
def get_thumb_path(
493-
self, file_id, thumbnail_frame, clear_cache=False):
494-
"""Get thumbnail path by invoking HTTP thumbnail request"""
495-
496-
# Clear thumb cache (if requested)
497-
thumb_cache = ""
498-
if clear_cache:
499-
thumb_cache = "no-cache/"
500-
501-
# Connect to thumbnail server and get image
502-
thumb_server_details = get_app().window.http_server_thread.server_address
503-
thumb_address = "http://%s:%s/thumbnails/%s/%s/path/%s" % (
504-
thumb_server_details[0],
505-
thumb_server_details[1],
506-
file_id,
507-
thumbnail_frame,
508-
thumb_cache)
509-
r = get(thumb_address)
510-
if r.ok:
511-
# Update thumbnail path to real one
512-
return r.text
513-
else:
514-
return ''
515-
516488
def update_file_thumbnail(self, file_id):
517489
"""Update/re-generate the thumbnail of a specific file"""
518490
file = File.get(id=file_id)
@@ -540,7 +512,7 @@ def update_file_thumbnail(self, file_id):
540512
thumbnail_frame = round(float(file.data['start']) * fps_float) + 1
541513

542514
# Get thumb path
543-
thumb_icon = QIcon(self.get_thumb_path(file.id, thumbnail_frame, clear_cache=True))
515+
thumb_icon = QIcon(GetThumbPath(file.id, thumbnail_frame, clear_cache=True))
544516
else:
545517
# Audio file
546518
thumb_icon = QIcon(os.path.join(info.PATH, "images", "AudioThumbnail.svg"))

src/windows/views/webview.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
from classes.query import File, Clip, Transition, Track
4848
from classes.waveform import get_audio_data
4949
from classes.effect_init import effect_options
50+
from classes.thumbnail import GetThumbPath
51+
5052

5153
# Constants used by this file
5254
JS_SCOPE_SELECTOR = "$('body').scope()"
@@ -1105,10 +1107,15 @@ def clipAudioDataReady_Triggered(self, clip_id, ui_data, tid):
11051107
# Clear transaction id
11061108
get_app().updates.transaction_id = None
11071109

1108-
def Thumbnail_Updated(self, clip_id):
1110+
def Thumbnail_Updated(self, clip_id, thumbnail_frame=1):
11091111
"""Callback when thumbnail needs to be updated"""
1110-
# Pass to javascript timeline (and render)
1111-
self.run_js(JS_SCOPE_SELECTOR + ".updateThumbnail('" + clip_id + "');")
1112+
clips = Clip.filter(id=clip_id)
1113+
for clip in clips:
1114+
# Force thumbnail image to be refreshed (for a particular frame #)
1115+
GetThumbPath(clip.data.get("file_id"), thumbnail_frame, clear_cache=True)
1116+
1117+
# Pass to javascript timeline (and render)
1118+
self.run_js(JS_SCOPE_SELECTOR + ".updateThumbnail('" + clip_id + "');")
11121119

11131120
def Split_Audio_Triggered(self, action, clip_ids):
11141121
"""Callback for split audio context menus"""
@@ -2995,6 +3002,8 @@ def callback(self, data, callback_data):
29953002
new_clip["start"] = file.data['start']
29963003
if 'end' in file.data:
29973004
new_clip["end"] = file.data['end']
3005+
else:
3006+
new_clip["end"] = new_clip["reader"]["duration"]
29983007

29993008
# Set position and closet track
30003009
new_clip["position"] = js_position

0 commit comments

Comments
 (0)