Skip to content

Commit 2bc3645

Browse files
authored
Merge pull request #185 from kieranjol/copyit
Copyit
2 parents 388d9c3 + eba9a5c commit 2bc3645

File tree

8 files changed

+1249
-452
lines changed

8 files changed

+1249
-452
lines changed

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,12 @@ Note: Documentation template has been copied from [mediamicroservices](https://g
6161
## Arrangement ##
6262

6363
### sipcreator.py ###
64-
* Accepts one or more directories as input and wraps them up in a directory structure in line with IFI procedures using `moveit.py`.
65-
* Folders will be stored in an objects directory. Directory structure is a parent directory named with a UUID, with three child directories (objects, logs metadata):
66-
* Checksums are stored for the package and metadata is extracted for the AV material in the objects. A log records the major events in the process.
64+
* Accepts one or more files or directories as input and wraps them up in a directory structure in line with IFI procedures using `copyit.py`.
65+
* Source objects will be stored in an /objects directory. Directory structure is: parent directory named with a UUID, with three child directories (objects, logs metadata):
66+
* Metadata is extracted for the AV material and MD5 checksums are stored for the entire package. A log records the major events in the process.
6767
* Usage for one directory - `sipcreator.py -i /path/to/directory_name -o /path/to/output_folder`
6868
* Usage for more than one directory - `sipcreator.py -i /path/to/directory_name1 /path/to/directory_name2 -o /path/to/output_folder`
69+
* Run `sipcreator.py -h` for all options.
6970

7071
## Transcodes ##
7172

@@ -87,10 +88,11 @@ Note: Documentation template has been copied from [mediamicroservices](https://g
8788
* This script has many extra options, such as deinterlacing, quality settings, rescaling. Use `prores.py -h` to see all options
8889

8990
### concat.py ###
90-
* Concatenate/join video files together using ffmpeg stream copy into a single Matroska container. As the streams are losslessly copied, the speed is quite fast.
91+
* Concatenate/join video files together using ffmpeg stream copy into a single Matroska container. Each source clip will have its own chapter marker. As the streams are copied, the speed is quite fast.
9192
* Usage: `concat.py -i /path/to/filename1.mov /path/to/filename2.mov -o /path/to/destination_folder`
93+
* A lossless verification process will also run, which takes stream level checksums of all streams and compares the values. This is not very reliable at the moment.
9294
* Warning - video files must have the same technical attributes such as codec, width, height, fps. Some characters in filenames will cause the script to fail. Some of these include quotes. The script will ask the user if quotes should be renamed with underscores. Also, a temporary concatenation textfile will be stored in your temp folder. Currently only tested on Ubuntu.
93-
95+
* Dependencies: mkvpropedit, ffmpeg.
9496
## Digital Cinema Package Scripts ##
9597

9698
### dcpaccess.py ###
@@ -112,8 +114,8 @@ Note: Documentation template has been copied from [mediamicroservices](https://g
112114

113115
## Fixity Scripts ##
114116

115-
### moveit.py ###
116-
* Copies a directory, creating a md5 manifest at source and destination and comparing the two. Skips hidden files and directories.
117+
### copyit.py ###
118+
* Copies a file or directory, creating a md5 manifest at source and destination and comparing the two. Skips hidden files and directories.
117119
* Usage: ` moveit.py source_dir destination_dir`
118120
* Dependencies: OSX requires gcp - `brew install coreutils`
119121

concat.py

Lines changed: 147 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import subprocess
77
import os
88
import argparse
9+
import time
10+
import shutil
11+
import sipcreator
912
import ififuncs
1013

11-
1214
def parse_args(args_):
1315
'''
1416
Parse command line arguments.
@@ -29,6 +31,17 @@ def parse_args(args_):
2931
'-r', '-recursive',
3032
help='recursively process all files in subdirectories. This could be potentially a disaster - so use with caution or with XDCAM', action='store_true'
3133
)
34+
parser.add_argument(
35+
'--no-sip',
36+
help='Do not run sipcreator.py on the resulting file.', action='store_true'
37+
)
38+
parser.add_argument(
39+
'-user',
40+
help='Declare who you are. If this is not set, you will be prompted.')
41+
parser.add_argument(
42+
'-oe',
43+
help='Enter the Object Entry number for the representation.SIP will be placed in a folder with this name.'
44+
)
3245
parsed_args = parser.parse_args(args_)
3346
return parsed_args
3447

@@ -37,17 +50,22 @@ def ffmpeg_concat(concat_file, args, uuid):
3750
'''
3851
Launch the actual ffmpeg concatenation command
3952
'''
53+
fmd5_logfile = os.path.join(args.o, '%s_concat.log' % uuid).replace('\\', '\\\\').replace(':', '\:')
54+
fmd5_env_dict = ififuncs.set_environment(fmd5_logfile)
55+
print fmd5_logfile
56+
print fmd5_env_dict
4057
cmd = [
41-
'ffmpeg', '-f', 'concat', '-safe', '0',
58+
'ffmpeg', '-report', '-f', 'concat', '-safe', '0',
4259
'-i', concat_file,
43-
'-c', 'copy', '-map', '0:a?', '-map', '0:v',
44-
os.path.join(args.o, '%s.mkv' % uuid)
60+
'-c', 'copy', '-map', '0:v', '-map', '0:a?',
61+
os.path.join(args.o, '%s.mkv' % uuid),
62+
'-f', 'md5', '-map', '0:v', '-map', '0:a?','-c', 'copy', '-'
4563
]
4664
print cmd
47-
subprocess.call(
48-
cmd
65+
source_bitstream_md5 = subprocess.check_output(
66+
cmd, env=fmd5_env_dict
4967
)
50-
68+
return source_bitstream_md5.rstrip(), fmd5_logfile.replace('\\\\', '\\').replace('\:', ':')
5169

5270
def recursive_file_list(video_files):
5371
'''
@@ -67,6 +85,25 @@ def recursive_file_list(video_files):
6785
print recursive_list
6886
return recursive_list
6987

88+
def make_chapters(video_files):
89+
millis = ififuncs.get_milliseconds(video_files[0])
90+
timestamp = ififuncs.convert_millis(int(millis))
91+
chapter_list = [['00:00:00.000', os.path.basename(video_files[0])], [timestamp, os.path.basename(video_files[1])]]
92+
count = 2
93+
for video in video_files[1:]:
94+
millis += ififuncs.get_milliseconds(video)
95+
timestamp = ififuncs.convert_millis(int(millis))
96+
if count == len(video_files):
97+
continue
98+
else:
99+
chapter_list.append([timestamp, os.path.basename(video_files[count])])
100+
count+=1
101+
chapter_counter = 1
102+
# uh use a real path/filename.
103+
with open('chapters.txt', 'wb') as fo:
104+
for i in chapter_list:
105+
fo.write('CHAPTER%s=%s\nCHAPTER%sNAME=%s\n' % (str(chapter_counter).zfill(2), i[0], str(chapter_counter).zfill(2), i[1]))
106+
chapter_counter += 1
70107

71108

72109
def main(args_):
@@ -75,13 +112,115 @@ def main(args_):
75112
'''
76113
uuid = ififuncs.create_uuid()
77114
args = parse_args(args_)
115+
print args
116+
log_name_source = os.path.join(args.o, '%s_concat_log.log' % time.strftime("_%Y_%m_%dT%H_%M_%S"))
117+
ififuncs.generate_log(log_name_source, 'concat.py started.')
118+
ififuncs.generate_log(
119+
log_name_source,
120+
'eventDetail=concat.py %s' % ififuncs.get_script_version('concat.py'))
121+
ififuncs.generate_log(
122+
log_name_source,
123+
'Command line arguments: %s' % args
124+
)
125+
if args.user:
126+
user = args.user
127+
else:
128+
user = ififuncs.get_user()
129+
if args.oe:
130+
if args.oe[:2] != 'oe':
131+
print 'First two characters must be \'oe\' and last four characters must be four digits'
132+
object_entry = ififuncs.get_object_entry()
133+
elif len(args.oe[2:]) != 4:
134+
print 'First two characters must be \'oe\' and last four characters must be four digits'
135+
object_entry = ififuncs.get_object_entry()
136+
elif not args.oe[2:].isdigit():
137+
object_entry = ififuncs.get_object_entry()
138+
print 'First two characters must be \'oe\' and last four characters must be four digits'
139+
else:
140+
object_entry = args.oe
141+
else:
142+
object_entry = ififuncs.get_object_entry()
143+
ififuncs.generate_log(
144+
log_name_source,
145+
'EVENT = agentName=%s' % user
146+
)
147+
source_uuid_check = ififuncs.check_for_uuid(args)
148+
if source_uuid_check == False:
149+
source_uuid = ififuncs.get_source_uuid()
150+
else: source_uuid = source_uuid_check
151+
ififuncs.generate_log(
152+
log_name_source,
153+
'Relationship, derivation, has source=%s' % source_uuid
154+
)
78155
video_files = args.i
79156
concat_file = ififuncs.get_temp_concat('concat_stuff')
157+
ififuncs.generate_log(
158+
log_name_source,
159+
'concatenation file=%s' % concat_file)
80160
if args.r:
81161
video_files = recursive_file_list(video_files)
82162
video_files = ififuncs.sanitise_filenames(video_files)
163+
for source_files in video_files:
164+
ififuncs.generate_log(
165+
log_name_source,
166+
'source_files = %s' % source_files)
167+
make_chapters(video_files)
168+
ififuncs.concat_textfile(video_files, concat_file)
169+
ififuncs.generate_log(
170+
log_name_source,
171+
'EVENT = Concatenation, status=started, eventType=Creation, agentName=ffmpeg, eventDetail=Source media concatenated into a single file output=%s' % os.path.join(args.o, '%s.mkv' % uuid))
172+
source_bitstream_md5, fmd5_logfile = ffmpeg_concat(concat_file, args, uuid)
173+
output_file = os.path.join(args.o, '%s.mkv' % uuid)
174+
ififuncs.generate_log(
175+
log_name_source,
176+
'EVENT = Concatenation, status=finished, eventType=Creation, agentName=ffmpeg, eventDetail=Source media concatenated into a single file output=%s' % os.path.join(args.o, '%s.mkv' % uuid))
177+
ififuncs.generate_log(
178+
log_name_source,
179+
'EVENT = losslessness verification, status=started, eventType=messageDigestCalculation, agentName=ffmpeg, eventDetail=MD5s of AV streams of output file generated for validation')
180+
validation_logfile = os.path.join(args.o, '%s_validation.log' % uuid).replace('\\', '\\\\').replace(':', '\:')
181+
validation_env_dict = ififuncs.set_environment(validation_logfile)
182+
output_bitstream_md5 = subprocess.check_output([
183+
'ffmpeg', '-report',
184+
'-i', output_file,
185+
'-f', 'md5', '-map', '0:v', '-map', '0:a?', '-c', 'copy', '-'
186+
], env=validation_env_dict).rstrip()
187+
ififuncs.generate_log(
188+
log_name_source,
189+
'EVENT = losslessness verification, status=finished, eventType=messageDigestCalculation, agentName=ffmpeg, eventDetail=MD5s of AV streams of output file generated for validation')
190+
if source_bitstream_md5 == output_bitstream_md5:
191+
print 'process appears to be lossless'
192+
print source_bitstream_md5, output_bitstream_md5
193+
ififuncs.generate_log(
194+
log_name_source,
195+
'EVENT = losslessness verification, eventOutcome=pass')
196+
else:
197+
print 'something went wrong - not lossless!'
198+
print source_bitstream_md5,output_bitstream_md5
199+
ififuncs.generate_log(
200+
log_name_source,
201+
'EVENT = losslessness verification, eventOutcome=fail')
202+
subprocess.call(['mkvpropedit', output_file, '-c', 'chapters.txt'])
203+
ififuncs.generate_log(
204+
log_name_source,
205+
'EVENT = eventType=modification, agentName=mkvpropedit, eventDetail=Chapters added to file detailing start point of source clips.')
83206
ififuncs.concat_textfile(video_files, concat_file)
84-
ffmpeg_concat(concat_file, args, uuid)
207+
with open(log_name_source, 'r') as concat_log:
208+
concat_lines = concat_log.readlines()
209+
if not args.no_sip:
210+
sipcreator_log, sipcreator_manifest = sipcreator.main(['-i', output_file, '-u', uuid, '-oe', object_entry, '-user', user, '-o', args.o])
211+
shutil.move(fmd5_logfile, os.path.dirname(sipcreator_log))
212+
shutil.move(validation_logfile.replace('\\\\', '\\').replace('\:', ':'), os.path.dirname(sipcreator_log))
213+
logs_dir = os.path.dirname(sipcreator_log)
214+
ififuncs.manifest_update(sipcreator_manifest, os.path.join(logs_dir, os.path.basename(fmd5_logfile)))
215+
ififuncs.manifest_update(sipcreator_manifest, os.path.join(logs_dir,(os.path.basename(validation_logfile.replace('\\\\', '\\').replace('\:', ':')))))
216+
with open(sipcreator_log, 'r') as sipcreator_log_object:
217+
sipcreator_lines = sipcreator_log_object.readlines()
218+
with open(sipcreator_log, 'wb') as fo:
219+
for lines in concat_lines:
220+
fo.write(lines)
221+
for remaining_lines in sipcreator_lines:
222+
fo.write(remaining_lines)
223+
ififuncs.checksum_replace(sipcreator_manifest, sipcreator_log)
85224

86225
if __name__ == '__main__':
87226
main(sys.argv[1:])

0 commit comments

Comments
 (0)