Skip to content

Commit

Permalink
Added --relink-by-name feature to otiotool (#1475)
Browse files Browse the repository at this point in the history
Signed-off-by: Joshua Minor <[email protected]>
  • Loading branch information
jminor authored Nov 23, 2022
1 parent 5e5284a commit 33e0d1a
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 7 deletions.
76 changes: 71 additions & 5 deletions src/py-opentimelineio/opentimelineio/console/otiotool.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import argparse
import os
import pathlib
import re
import sys

Expand Down Expand Up @@ -90,6 +91,11 @@ def main():

# Phase 5: Relinking media

if args.relink_by_name:
for timeline in timelines:
for folder in args.relink_by_name:
relink_by_name(timeline, folder)

if args.copy_media_to_folder:
for timeline in timelines:
copy_media_to_folder(timeline, args.copy_media_to_folder)
Expand Down Expand Up @@ -162,6 +168,10 @@ def parse_arguments():
performed (in that order) to combine all of the input timeline(s) into one.
4. Relink
The --relink-by-name option, will scan the specified folder(s) looking for
files which match the name of each clip in the input timeline(s).
If matching files are found, clips will be relinked to those files (using
file:// URLs). Clip names are matched to filenames ignoring file extension.
If specified, the --copy-media-to-folder option, will copy or download
all linked media, and relink the OTIO to reference the local copies.
Expand Down Expand Up @@ -204,6 +214,7 @@ def parse_arguments():
type=str,
nargs='+',
required=True,
metavar='PATH(s)',
help="""Input file path(s). All formats supported by adapter plugins
are supported. Use '-' to read OTIO from standard input."""
)
Expand All @@ -224,26 +235,30 @@ def parse_arguments():
parser.add_argument(
"--only-tracks-with-name",
type=str,
nargs='*',
nargs='+',
metavar='NAME(s)',
help="Output tracks with these name(s)"
)
parser.add_argument(
"--only-tracks-with-index",
type=int,
nargs='*',
nargs='+',
metavar='INDEX(es)',
help="Output tracks with these indexes"
" (1 based, in same order as --list-tracks)"
)
parser.add_argument(
"--only-clips-with-name",
type=str,
nargs='*',
nargs='+',
metavar='NAME(s)',
help="Output only clips with these name(s)"
)
parser.add_argument(
"--only-clips-with-name-regex",
type=str,
nargs='*',
nargs='+',
metavar='REGEX(es)',
help="Output only clips with names matching the given regex"
)
parser.add_argument(
Expand All @@ -256,6 +271,7 @@ def parse_arguments():
"--trim",
type=str,
nargs=2,
metavar=('START', 'END'),
help="Trim from <start> to <end> as HH:MM:SS:FF timecode or seconds"
)

Expand All @@ -264,6 +280,7 @@ def parse_arguments():
"-f",
"--flatten",
choices=['video', 'audio', 'all'],
metavar='TYPE',
help="Flatten multiple tracks into one."
)
parser.add_argument(
Expand All @@ -286,9 +303,18 @@ def parse_arguments():
)

# Relink
parser.add_argument(
"--relink-by-name",
type=str,
nargs='+',
metavar='FOLDER(s)',
help="""Scan the specified folder looking for filenames which match
each clip's name. If found, clips are relinked to those files."""
)
parser.add_argument(
"--copy-media-to-folder",
type=str,
metavar='FOLDER',
help="""Copy or download all linked media to the specified folder and
relink all media references to the copies"""
)
Expand Down Expand Up @@ -337,7 +363,8 @@ def parse_arguments():
parser.add_argument(
"--inspect",
type=str,
nargs='*',
nargs='+',
metavar='NAME(s)',
help="Inspect details of clips with names matching the given regex"
)

Expand All @@ -346,6 +373,7 @@ def parse_arguments():
"-o",
"--output",
type=str,
metavar='PATH',
help="""Output file. All formats supported by adapter plugins
are supported. Use '-' to write OTIO to standard output."""
)
Expand Down Expand Up @@ -605,6 +633,44 @@ def copy_media(url, destination_path):
return destination_path


def relink_by_name(timeline, path):
"""Relink clips in the timeline to media files discovered at the
given folder path."""

def _conform_path(p):
# Turn absolute paths into file:// URIs
if os.path.isabs(p):
return pathlib.Path(p).as_uri()
else:
# Leave relative paths as-is
return p

count = 0
if os.path.isdir(path):
name_to_url = dict([
(
os.path.splitext(x)[0],
_conform_path(os.path.join(path, x))
)
for x in os.listdir(path)
])
elif os.path.isfile(path):
print((f"ERROR: Cannot relink to '{path}':"
" Please specify a folder instead of a file."))
return
else:
print(f"ERROR: Cannot relink to '{path}': No such file or folder.")
return

for clip in timeline.each_clip():
url = name_to_url.get(clip.name)
if url is not None:
clip.media_reference = otio.schema.ExternalReference(target_url=url)
count += 1

print(f"Relinked {count} clips to files in folder {path}")


def copy_media_to_folder(timeline, folder):
"""Copy or download all referenced media to this folder, and relink media
references to the copies."""
Expand Down
31 changes: 29 additions & 2 deletions tests/test_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import os
import subprocess
import sysconfig
import pathlib
import platform

import io
Expand All @@ -20,9 +21,11 @@
import opentimelineio.console as otio_console

SAMPLE_DATA_DIR = os.path.join(os.path.dirname(__file__), "sample_data")
SCREENING_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "screening_example.edl")
PREMIERE_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "premiere_example.xml")

MULTITRACK_PATH = os.path.join(SAMPLE_DATA_DIR, "multitrack.otio")
PREMIERE_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "premiere_example.xml")
SCREENING_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "screening_example.edl")
SIMPLE_CUT_PATH = os.path.join(SAMPLE_DATA_DIR, "simple_cut.otio")
TRANSITION_PATH = os.path.join(SAMPLE_DATA_DIR, "transition.otio")


Expand Down Expand Up @@ -894,6 +897,30 @@ def test_inspect(self):
" range in NestedScope (<class 'opentimelineio._otio.Stack'>): TimeRange(RationalTime(1198, 24), RationalTime(640, 24))\n"), # noqa E501 line too long
out)

def test_relink(self):
with tempfile.TemporaryDirectory() as temp_dir:
temp_file1 = os.path.join(temp_dir, "Clip-001.empty")
temp_file2 = os.path.join(temp_dir, "Clip-003.empty")
open(temp_file1, "w").write("A")
open(temp_file2, "w").write("B")

temp_url = pathlib.Path(temp_dir).as_uri()

sys.argv = [
'otiotool',
'-i', SIMPLE_CUT_PATH,
'--relink-by-name', temp_dir,
'--list-media'
]
out, err = self.run_test()
self.assertIn(
("TIMELINE: Figure 1 - Simple Cut List\n"
f" MEDIA: {temp_url}/Clip-001.empty\n"
" MEDIA: file:///folder/wind-up.mov\n"
f" MEDIA: {temp_url}/Clip-003.empty\n"
" MEDIA: file:///folder/credits.mov\n"),
out)


OTIOToolTest_ShellOut = CreateShelloutTest(OTIOToolTest)

Expand Down

0 comments on commit 33e0d1a

Please sign in to comment.