Skip to content

Commit

Permalink
Merge pull request #3630 from ferdnyc/import-folder
Browse files Browse the repository at this point in the history
Project Files: Recursive folder import (drag-and-drop only)
  • Loading branch information
ferdnyc authored Oct 17, 2020
2 parents 4be95e1 + 90ce3ee commit 685326f
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 123 deletions.
37 changes: 21 additions & 16 deletions src/windows/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,22 +755,27 @@ def actionSaveAs_trigger(self, event):
def actionImportFiles_trigger(self, event):
app = get_app()
_ = app._tr

recommended_path = app.project.get("import_path")
if not recommended_path or not os.path.exists(recommended_path):
recommended_path = os.path.join(info.HOME_PATH)
files = QFileDialog.getOpenFileNames(self, _("Import File..."), recommended_path)[0]

# Set cursor to waiting
get_app().setOverrideCursor(QCursor(Qt.WaitCursor))
qurl_list = QFileDialog.getOpenFileUrls(
self, _("Import Files..."),
QUrl.fromLocalFile(recommended_path))[0]

# Import list of files
self.files_model.add_files(files)
# Set cursor to waiting
app.setOverrideCursor(QCursor(Qt.WaitCursor))

# Restore cursor
get_app().restoreOverrideCursor()
try:
# Import list of files
self.files_model.process_urls(qurl_list)

# Refresh files views
self.refreshFilesSignal.emit()
# Refresh files views
self.refreshFilesSignal.emit()
finally:
# Restore cursor
app.restoreOverrideCursor()

def invalidImage(self, filename=None):
""" Show a popup when an image file can't be loaded """
Expand Down Expand Up @@ -1387,9 +1392,9 @@ def getTimelineObjectPositions(obj):
# Add all Effect keyframes
if "effects" in obj.data:
for effect_data in obj.data["effects"]:
for property in effect_data:
for prop in effect_data:
try:
for point in effect_data[property]["Points"]:
for point in effect_data[prop]["Points"]:
keyframe_time = (point["co"]["X"]-1)/fps_float + clip_orig_time
if clip_start_time < keyframe_time < clip_stop_time:
positions.append(keyframe_time)
Expand Down Expand Up @@ -1773,12 +1778,12 @@ def actionRemove_from_Project_trigger(self, event):
if not f:
continue

id = f.data["id"]
f_id = f.data["id"]
# Remove file
f.delete()

# Find matching clips (if any)
clips = Clip.filter(file_id=id)
clips = Clip.filter(file_id=f_id)
for c in clips:
# Remove clip
c.delete()
Expand Down Expand Up @@ -2084,14 +2089,14 @@ def showDocks(self, docks):
dock.show()

def freezeDocks(self):
""" Freeze all dockable widgets on the main screen.
""" Freeze all dockable widgets on the main screen
(prevent them being closed, floated, or moved) """
for dock in self.getDocks():
if self.dockWidgetArea(dock) != Qt.NoDockWidgetArea:
dock.setFeatures(QDockWidget.NoDockWidgetFeatures)

def unFreezeDocks(self):
""" Un-freeze all dockable widgets on the main screen.
""" Un-freeze all dockable widgets on the main screen
(allow them to be closed, floated, or moved, as appropriate) """
for dock in self.getDocks():
if self.dockWidgetArea(dock) != Qt.NoDockWidgetArea:
Expand Down Expand Up @@ -2393,7 +2398,7 @@ def load_settings(self):
if s.get('window_geometry_v2'):
self.restoreGeometry(qt_types.str_to_bytes(s.get('window_geometry_v2')))
if s.get('docks_frozen'):
""" Freeze all dockable widgets on the main screen """
# Freeze all dockable widgets on the main screen
self.freezeDocks()
self.actionFreeze_View.setVisible(False)
self.actionUn_Freeze_View.setVisible(True)
Expand Down
157 changes: 102 additions & 55 deletions src/windows/models/files_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import json
import re
import glob
import functools

from PyQt5.QtCore import (
QMimeData, Qt, pyqtSignal, QEventLoop, QObject,
Expand Down Expand Up @@ -126,7 +127,7 @@ def changed(self, action):
self.update_model(clear=True)

def update_model(self, clear=True, delete_file_id=None):
log.info("updating files model.")
log.debug("updating files model.")
app = get_app()

self.ignore_updates = True
Expand Down Expand Up @@ -246,7 +247,7 @@ def update_model(self, clear=True, delete_file_id=None):
# Emit signal when model is updated
self.ModelRefreshed.emit()

def add_files(self, files, image_seq_details=None):
def add_files(self, files, image_seq_details=None, quiet=False):
# Access translations
app = get_app()
_ = app._tr
Expand All @@ -259,11 +260,12 @@ def add_files(self, files, image_seq_details=None):
(dir_path, filename) = os.path.split(filepath)

# Check for this path in our existing project data
file = File.get(path=filepath)
new_file = File.get(path=filepath)

# If this file is already found, exit
if file:
return
if new_file:
del new_file
continue

try:
# Load filepath in libopenshot clip object (which will try multiple readers to open it)
Expand All @@ -285,21 +287,19 @@ def add_files(self, files, image_seq_details=None):
file_data["media_type"] = "video"

# Save new file to the project data
file = File()
file.data = file_data

if not image_seq_details:
# Try to discover image sequence, if not supplied
image_seq_details = self.get_image_sequence_details(filepath)
new_file = File()
new_file.data = file_data

# Is this an image sequence / animation?
if image_seq_details:
seq_info = image_seq_details or self.get_image_sequence_details(filepath)

if seq_info:
# Update file with correct path
folder_path = image_seq_details["folder_path"]
base_name = image_seq_details["base_name"]
fixlen = image_seq_details["fixlen"]
digits = image_seq_details["digits"]
extension = image_seq_details["extension"]
folder_path = seq_info["folder_path"]
base_name = seq_info["base_name"]
fixlen = seq_info["fixlen"]
digits = seq_info["digits"]
extension = seq_info["extension"]

if not fixlen:
zero_pattern = "%d"
Expand All @@ -313,32 +313,34 @@ def add_files(self, files, image_seq_details=None):
folderName = os.path.basename(folder_path)
if not base_name:
# Give alternate name
file.data["name"] = "%s (%s)" % (folderName, pattern)
new_file.data["name"] = "%s (%s)" % (folderName, pattern)

# Load image sequence (to determine duration and video_length)
image_seq = openshot.Clip(os.path.join(folder_path, pattern))

# Update file details
file.data["path"] = os.path.join(folder_path, pattern)
file.data["media_type"] = "video"
file.data["duration"] = image_seq.Reader().info.duration
file.data["video_length"] = image_seq.Reader().info.video_length
new_file.data["path"] = os.path.join(folder_path, pattern)
new_file.data["media_type"] = "video"
new_file.data["duration"] = image_seq.Reader().info.duration
new_file.data["video_length"] = image_seq.Reader().info.video_length

log.info('Imported {} as image sequence {}'.format(
filepath, pattern))

# Remove any other image sequence files from the list we're processing
match_glob = "{}{}.{}".format(base_name, '[0-9]*', extension)
log.debug("Removing files from import list with glob: {}".format(match_glob))
for seq_file in glob.iglob(os.path.join(folder_path, match_glob)):
if seq_file in files:
# Don't remove the current file, or we mess up the for loop
if seq_file in files and seq_file != filepath:
files.remove(seq_file)

if not image_seq_details:
if not seq_info:
# Log our not-an-image-sequence import
log.info("Imported media file {}".format(filepath))

# Save file
file.save()
new_file.save()

prev_path = app.project.get("import_path")
if dir_path != prev_path:
Expand All @@ -348,8 +350,9 @@ def add_files(self, files, image_seq_details=None):
# Log exception
log.warning("Failed to import {}: {}".format(filepath, ex))

# Show message box to user
app.window.invalidImage(filename)
if not quiet:
# Show message box to user
app.window.invalidImage(filename)

# Reset list of ignored paths
self.ignore_image_sequence_paths = []
Expand All @@ -359,6 +362,11 @@ def get_image_sequence_details(self, file_path):

# Get just the file name
(dirName, fileName) = os.path.split(file_path)

# Image sequence imports are one per directory per run
if dirName in self.ignore_image_sequence_paths:
return None

extensions = ["png", "jpg", "jpeg", "gif", "tif", "svg"]
match = re.findall(r"(.*[^\d])?(0*)(\d+)\.(%s)" % "|".join(extensions), fileName, re.I)

Expand All @@ -385,34 +393,67 @@ def get_image_sequence_details(self, file_path):
for x in range(max(0, number - 100), min(number + 101, 50000)):
if x != number and os.path.exists(
"%s%s.%s" % (full_base_name, str(x).rjust(digits, "0") if fixlen else str(x), extension)):
is_sequence = True
break
break # found one!
else:
is_sequence = False

if is_sequence and dirName not in self.ignore_image_sequence_paths:
log.info('Prompt user to import image sequence from {}'.format(dirName))
# Ignore this path (temporarily)
self.ignore_image_sequence_paths.append(dirName)

if not get_app().window.promptImageSequence(fileName):
# User said no, don't import as a sequence
return None

# Yes, import image sequence
parameters = {
"folder_path": dirName,
"base_name": base_name,
"fixlen": fixlen,
"digits": digits,
"extension": extension
}
return parameters

# We didn't discover an image sequence
return None

def get_thumb_path(self, file_id, thumbnail_frame, clear_cache=False):
# We didn't discover an image sequence
return None

# Found a sequence, ignore this path (no matter what the user answers)
# To avoid issues with overlapping/conflicting sets of files,
# we only attempt one image sequence match per directory
log.debug("Ignoring path for image sequence imports: {}".format(dirName))
self.ignore_image_sequence_paths.append(dirName)

log.info('Prompt user to import sequence starting from {}'.format(fileName))
if not get_app().window.promptImageSequence(fileName):
# User said no, don't import as a sequence
return None

# Yes, import image sequence
parameters = {
"folder_path": dirName,
"base_name": base_name,
"fixlen": fixlen,
"digits": digits,
"extension": extension
}
return parameters

def process_urls(self, qurl_list):
"""Recursively process QUrls from a QDropEvent"""
import_quietly = False
media_paths = []

for uri in qurl_list:
filepath = uri.toLocalFile()
if not os.path.exists(filepath):
continue
if filepath.endswith(".osp") and os.path.isfile(filepath):
# Auto load project passed as argument
self.win.OpenProjectSignal.emit(filepath)
return True
if os.path.isdir(filepath):
import_quietly = True
log.info("Recursively importing {}".format(filepath))
try:
for r, _, f in os.walk(filepath):
media_paths.extend(
[os.path.join(r, p) for p in f])
except OSError:
log.warning("Directory recursion failed", exc_info=1)
elif os.path.isfile(filepath):
media_paths.append(filepath)

# Import all new media files
if media_paths:
media_paths.sort()
log.debug("Importing file list: {}".format(media_paths))
return self.add_files(media_paths, quiet=import_quietly)
else:
return False

def get_thumb_path(
self, file_id, thumbnail_frame, clear_cache=False):
"""Get thumbnail path by invoking HTTP thumbnail request"""

# Clear thumb cache (if requested)
Expand All @@ -423,7 +464,11 @@ def get_thumb_path(self, file_id, thumbnail_frame, clear_cache=False):
# Connect to thumbnail server and get image
thumb_server_details = get_app().window.http_server_thread.server_address
thumb_address = "http://%s:%s/thumbnails/%s/%s/path/%s" % (
thumb_server_details[0], thumb_server_details[1], file_id, thumbnail_frame, thumb_cache)
thumb_server_details[0],
thumb_server_details[1],
file_id,
thumbnail_frame,
thumb_cache)
r = get(thumb_address)
if r.ok:
# Update thumbnail path to real one
Expand Down Expand Up @@ -519,6 +564,8 @@ def __init__(self, *args):

# Connect signal
app.window.FileUpdated.connect(self.update_file_thumbnail)
app.window.refreshFilesSignal.connect(
functools.partial(self.update_model, clear=False))

# Call init for superclass QObject
super(QObject, FilesModel).__init__(self, *args)
Expand Down
Loading

0 comments on commit 685326f

Please sign in to comment.