Skip to content
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

Add support for ImageSequenceReference to otioz and otiod adapters #1627

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
150 changes: 83 additions & 67 deletions src/py-opentimelineio/opentimelineio/adapters/file_bundle_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,9 @@ def _prepped_otio_for_bundle_and_manifest(
):
""" Create a new OTIO based on input_otio that has had media references
replaced according to the media_policy. Return that new OTIO and a
mapping of all the absolute file paths (not URLs) to be used in the bundle,
mapped to MediaReferences associated with those files. Media references in
the OTIO will be relinked by the adapters to point to their output
locations.
list of all the absolute file paths (not URLs) to be used in the bundle.
Media references in the OTIO will be relinked by the adapters to point to
their output locations.

The otio[dz] adapters use this function to do further relinking and build
their bundles.
Expand All @@ -91,7 +90,7 @@ def _prepped_otio_for_bundle_and_manifest(
# make sure the incoming OTIO isn't edited
result_otio = copy.deepcopy(input_otio)

path_to_reference_map = {}
paths = set()
invalid_files = set()

# result_otio is manipulated in place
Expand All @@ -103,68 +102,85 @@ def _prepped_otio_for_bundle_and_manifest(
)
continue

try:
target_url = cl.media_reference.target_url
except AttributeError:
# not an ExternalReference, ignoring it.
continue

parsed_url = urlparse.urlparse(target_url)

# ensure that the urlscheme is either file or ""
# file means "absolute path"
# none is interpreted as a relative path, relative to cwd
if parsed_url.scheme not in ("file", ""):
if media_policy is MediaReferencePolicy.ErrorIfNotFile:
raise NotAFileOnDisk(
"The {} adapter only works with media reference"
" target_url attributes that begin with 'file:'. Got a "
"target_url of: '{}'".format(adapter_name, target_url)
)
if media_policy is MediaReferencePolicy.MissingIfNotFile:
cl.media_reference = reference_cloned_and_missing(
cl.media_reference,
"target_url is not a file scheme url (start with url:)"
)
continue

# get an absolute path to the target file
target_file = os.path.abspath(url_utils.filepath_from_url(target_url))

# if the file hasn't already been checked
if (
target_file not in path_to_reference_map
and target_file not in invalid_files
and (
not os.path.exists(target_file)
or not os.path.isfile(target_file)
)
):
invalid_files.add(target_file)

if target_file in invalid_files:
if media_policy is MediaReferencePolicy.ErrorIfNotFile:
raise NotAFileOnDisk(target_file)
if media_policy is MediaReferencePolicy.MissingIfNotFile:
cl.media_reference = reference_cloned_and_missing(
cl.media_reference,
"target_url target is not a file or does not exist"
)

# do not need to relink it in the future or add this target to
# the manifest, because the path is either not a file or does
# not exist.
continue

# add the media reference to the list of references that point at this
# file, they will need to be relinked
path_to_reference_map.setdefault(target_file, []).append(
cl.media_reference
)

_guarantee_unique_basenames(path_to_reference_map.keys(), adapter_name)

return result_otio, path_to_reference_map
for mr in cl.media_references().values():
if isinstance(mr, schema.ImageSequenceReference):
url = cl.media_reference.target_url_base
else:
try:
url = cl.media_reference.target_url
except AttributeError:
# not an ImageSequenceReference or object with target_url,
# ignoring it.
continue

parsed_url = urlparse.urlparse(url)

# ensure that the urlscheme is either file or ""
# file means "absolute path"
# none is interpreted as a relative path, relative to cwd
if parsed_url.scheme not in ("file", ""):
if media_policy is MediaReferencePolicy.ErrorIfNotFile:
raise NotAFileOnDisk(
"The {} adapter only works with file URLs."
" Got a URL of: '{}'".format(adapter_name, url)
Comment on lines +124 to +125
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor note, update to f-string

Suggested change
"The {} adapter only works with file URLs."
" Got a URL of: '{}'".format(adapter_name, url)
f"The {adapter_name} adapter only works with file URLs."
f" Got a URL of: '{url}'"

)
if media_policy is MediaReferencePolicy.MissingIfNotFile:
cl.media_reference = reference_cloned_and_missing(
cl.media_reference,
"not a file URL"
)
continue

# get absolute paths to the target files
target_files = []
if isinstance(mr, schema.ImageSequenceReference):
for number in range(mr.number_of_images_in_sequence()):
target_files.append(os.path.abspath(
url_utils.filepath_from_url(
mr.target_url_for_image_number(number))))
else:
target_files.append(os.path.abspath(
url_utils.filepath_from_url(url)))

# if the file hasn't already been checked
for target_file in target_files:
if (
target_file not in paths
and target_file not in invalid_files
and (
not os.path.exists(target_file)
or not os.path.isfile(target_file)
)
):
invalid_files.add(target_file)

invalid = False
for target_file in target_files:
if target_file in invalid_files:
if media_policy is MediaReferencePolicy.ErrorIfNotFile:
raise NotAFileOnDisk(target_file)
invalid = True
break
if invalid:
if media_policy is MediaReferencePolicy.MissingIfNotFile:
cl.media_reference = reference_cloned_and_missing(
cl.media_reference,
"not a file URL or does not exist"
)

# do not need to relink it in the future or add this target to
# the manifest, because the path is either not a file or does
# not exist.
continue

# add the media reference to the list of references that point at this
# file, they will need to be relinked
for target_file in target_files:
paths.add(target_file)

_guarantee_unique_basenames(paths, adapter_name)

return result_otio, list(paths)


def _total_file_size_of(filepaths):
Expand Down
54 changes: 33 additions & 21 deletions src/py-opentimelineio/opentimelineio/adapters/otiod.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from .. import (
exceptions,
schema,
url_utils,
)

Expand Down Expand Up @@ -82,50 +83,61 @@ def write_to_file(
# -------------------------------------------------------------------------
# - build file manifest (list of paths to files on disk that will be put
# into the archive)
# - build a mapping of path to file on disk to url to put into the media
# reference in the result
# - build a mapping of input paths to output paths
# - relink the media references to point at the final location inside the
# archive
# - build the resulting structure (zip file, directory)
# -------------------------------------------------------------------------

result_otio, path_to_mr_map = utils._prepped_otio_for_bundle_and_manifest(
result_otio, paths = utils._prepped_otio_for_bundle_and_manifest(
input_otio,
media_policy,
"OTIOD"
)

# dryrun reports the total size of files
if dryrun:
return utils._total_file_size_of(path_to_mr_map.keys())
return utils._total_file_size_of(paths)

# get the output paths
abspath_to_output_path_map = {}

# relink all the media references to their target paths
for abspath, references in path_to_mr_map.items():
for abspath in paths:
target = os.path.join(
filepath,
utils.BUNDLE_DIR_NAME,
os.path.basename(abspath)
)
abspath_to_output_path_map[abspath] = target

# conform to posix style paths inside the bundle, so that they are
# portable between windows and *nix style environments
final_path = str(pathlib.Path(target).as_posix())

# cache the output path
abspath_to_output_path_map[abspath] = final_path

for mr in references:
# author the relative path from the root of the bundle in url
# form into the target_url
mr.target_url = url_utils.url_from_filepath(
os.path.relpath(final_path, filepath)
)
# relink all the media references to their target paths
for cl in result_otio.find_clips():
for mr in cl.media_references().values():
if isinstance(mr, schema.ImageSequenceReference):
url = cl.media_reference.target_url_base
else:
try:
url = cl.media_reference.target_url
except AttributeError:
continue

target = os.path.join(
utils.BUNDLE_DIR_NAME,
os.path.basename(url_utils.filepath_from_url(url)))

# conform to posix style paths inside the bundle, so that they are
# portable between windows and *nix style environments
final_path = str(pathlib.Path(target).as_posix())

# author the final_path in url form into the media reference
url = url_utils.url_from_filepath(final_path)
if isinstance(mr, schema.ImageSequenceReference):
mr.target_url_base = url if url.endswith('/') else url + '/'
else:
mr.target_url = url

os.mkdir(filepath)

# write the otioz file to the temp directory
# write the otio file
otio_json.write_to_file(
result_otio,
os.path.join(filepath, utils.BUNDLE_PLAYLIST_PATH)
Expand Down
49 changes: 32 additions & 17 deletions src/py-opentimelineio/opentimelineio/adapters/otioz.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from .. import (
exceptions,
schema,
url_utils,
)

Expand Down Expand Up @@ -83,39 +84,53 @@ def write_to_file(
# -------------------------------------------------------------------------
# - build file manifest (list of paths to files on disk that will be put
# into the archive)
# - build a mapping of path to file on disk to url to put into the media
# reference in the result
# - build a mapping of input paths to output paths
# - relink the media references to point at the final location inside the
# archive
# - build the resulting structure (zip file, directory)
# -------------------------------------------------------------------------

result_otio, path_to_mr_map = utils._prepped_otio_for_bundle_and_manifest(
result_otio, paths = utils._prepped_otio_for_bundle_and_manifest(
input_otio,
media_policy,
"OTIOZ"
)

# dryrun reports the total size of files
if dryrun:
return utils._total_file_size_of(path_to_mr_map.keys())
return utils._total_file_size_of(paths)

# get the output paths
abspath_to_output_path_map = {}

# relink all the media references to their target paths
for abspath, references in path_to_mr_map.items():
for abspath in paths:
target = os.path.join(utils.BUNDLE_DIR_NAME, os.path.basename(abspath))
abspath_to_output_path_map[abspath] = target

# conform to posix style paths inside the bundle, so that they are
# portable between windows and *nix style environments
final_path = str(pathlib.Path(target).as_posix())

# cache the output path
abspath_to_output_path_map[abspath] = final_path

for mr in references:
# author the final_path in url form into the target_url
mr.target_url = url_utils.url_from_filepath(final_path)
# relink all the media references to their target paths
for cl in result_otio.find_clips():
for mr in cl.media_references().values():
if isinstance(mr, schema.ImageSequenceReference):
url = cl.media_reference.target_url_base
else:
try:
url = cl.media_reference.target_url
except AttributeError:
continue

target = os.path.join(
utils.BUNDLE_DIR_NAME,
os.path.basename(url_utils.filepath_from_url(url)))

# conform to posix style paths inside the bundle, so that they are
# portable between windows and *nix style environments
final_path = str(pathlib.Path(target).as_posix())

# author the final_path in url form into the media reference
url = url_utils.url_from_filepath(final_path)
if isinstance(mr, schema.ImageSequenceReference):
mr.target_url_base = url if url.endswith('/') else url + '/'
else:
mr.target_url = url

# write the otioz file to the temp directory
otio_str = otio_json.write_to_string(result_otio)
Expand Down