Skip to content

Project Files: Recursive folder import (drag-and-drop only) #3630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 17, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 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.
(prevent them being closed, floated, or moved) """
""" 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
158 changes: 103 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_ile = 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,68 @@ 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 +465,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 +565,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