diff --git a/ptulsconv/__init__.py b/ptulsconv/__init__.py index 2df0032..3397969 100644 --- a/ptulsconv/__init__.py +++ b/ptulsconv/__init__.py @@ -2,6 +2,6 @@ from .ptuls_grammar import protools_text_export_grammar from .ptuls_parser_visitor import DictionaryParserVisitor from .transformations import TimecodeInterpreter -__version__ = '0.5.3' +__version__ = '0.6.0' __author__ = 'Jamie Hardt' __license__ = 'MIT' diff --git a/ptulsconv/__main__.py b/ptulsconv/__main__.py index e15db0f..d03a362 100644 --- a/ptulsconv/__main__.py +++ b/ptulsconv/__main__.py @@ -29,6 +29,15 @@ def main(): parser.add_option_group(filter_opts) + warn_options = OptionGroup(title="Warning and Validation Options", parser=parser) + warn_options.add_option('-W', action='store_true', dest='warnings', + help='Generate warnings for common errors (missing code numbers etc.)') + + warn_options.add_option('-S', action='store_true', dest='spelling', + help='Check spelling and warn on misspellings.') + + parser.add_option_group(warn_options) + output_opts = OptionGroup(title="Output Options", parser=parser) output_opts.add_option('--json', default=False, action='store_true', dest='write_json', help='Output a JSON document instead of XML. If this option is enabled, --xform will have ' @@ -90,7 +99,8 @@ def main(): convert(input_file=args[1], output_format=output_format, start=options.in_time, end=options.out_time, include_muted=options.include_muted, xsl=options.xslt, select_reel=options.select_reel, - progress=False, output=sys.stdout, log_output=sys.stderr) + progress=False, output=sys.stdout, log_output=sys.stderr, + warnings=options.warnings, spelling=options.spelling) except FileNotFoundError as e: print_fatal_error("Error trying to read input file") raise e diff --git a/ptulsconv/commands.py b/ptulsconv/commands.py index 00b0a51..8b9107e 100644 --- a/ptulsconv/commands.py +++ b/ptulsconv/commands.py @@ -7,8 +7,10 @@ from xml.etree.ElementTree import TreeBuilder, tostring import subprocess import pathlib import ptulsconv +from itertools import chain -from .reporting import print_section_header_style, print_status_style +from .reporting import print_section_header_style, print_status_style, print_warning +from .validations import * # field_map maps tags in the text export to fields in FMPXMLRESULT # - tuple field 0 is a list of tags, the first tag with contents will be used as source @@ -143,12 +145,12 @@ def fmp_transformed_dump(data, input_file, xsl_name, output): xsl_path = os.path.join(pathlib.Path(__file__).parent.absolute(), 'xslt', xsl_name + ".xsl") print_status_style("Using xsl: %s" % (xsl_path)) subprocess.run(['xsltproc', xsl_path, '-'], input=strdata, text=True, - stdout=output, shell=False, check=True) + stdout=output, shell=False, check=True) def convert(input_file, output_format='fmpxml', start=None, end=None, select_reel=None, progress=False, include_muted=False, xsl=None, - output=sys.stdout, log_output=sys.stderr): + output=sys.stdout, log_output=sys.stderr, warnings=False, spelling=False): with open(input_file, 'r') as file: print_section_header_style('Parsing') ast = ptulsconv.protools_text_export_grammar.parse(file.read()) @@ -184,6 +186,16 @@ def convert(input_file, output_format='fmpxml', start=None, end=None, select_ree parsed = reel_xform.transform(parsed) + if warnings: + for warning in chain(validate_unique_field(parsed, field='QN'), + validate_non_empty_field(parsed, field='QN'), + validate_non_empty_field(parsed, field='CN'), + validate_non_empty_field(parsed, field='Char'), + validate_dependent_value(parsed, key_field='CN', dependent_field='Char'), + validate_dependent_value(parsed, key_field='CN', dependent_field='Actor'),): + + print_warning(warning.report_message()) + if output_format == 'json': json.dump(parsed, output) elif output_format == 'fmpxml': diff --git a/ptulsconv/movie_export.py b/ptulsconv/movie_export.py new file mode 100644 index 0000000..bd7548b --- /dev/null +++ b/ptulsconv/movie_export.py @@ -0,0 +1,13 @@ +import ffmpeg # ffmpeg-python + + +def create_movie(event): + start = event['Movie.Start_Offset_Seconds'] + duration = event['PT.Clip.Finish_Seconds'] - event['PT.Clip.Start_Seconds'] + input_movie = event['Movie.Filename'] + print("Will make movie starting at {}, dur {} from movie {}".format(start, duration, input_movie)) + + +def export_movies(events): + for event in events: + create_movie(event) diff --git a/ptulsconv/reporting.py b/ptulsconv/reporting.py index 717e7c2..44e351b 100644 --- a/ptulsconv/reporting.py +++ b/ptulsconv/reporting.py @@ -12,12 +12,21 @@ def print_section_header_style(str): else: sys.stderr.write("%s\n\n" % str) + def print_status_style(str): if sys.stderr.isatty(): sys.stderr.write("\033[3m - %s\033[0m\n" % str) else: sys.stderr.write(" - %s\n" % str) + +def print_warning(warning_string): + if sys.stderr.isatty(): + sys.stderr.write("\033[3m - %s\033[0m\n" % warning_string) + else: + sys.stderr.write(" - %s\n" % warning_string) + + def print_advisory_tagging_error(failed_string, position, parent_track_name=None, clip_time=None): if sys.stderr.isatty(): sys.stderr.write("\n") diff --git a/ptulsconv/validations.py b/ptulsconv/validations.py new file mode 100644 index 0000000..3b04c04 --- /dev/null +++ b/ptulsconv/validations.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass +from sys import stderr + + +@dataclass +class ValidationError: + message: str + event: dict + + def report_message(self): + return f"{self.message}: event at {self.event['PT.Clip.Start']} on track {self.event['PT.Track.Name']}" + +def validate_value(input_dict, key_field, predicate): + for event in input_dict['events']: + val = event[key_field] + if not predicate(val): + yield ValidationError(message='Field {} not in range'.format(val), + event=event) + + +def validate_unique_field(input_dict, field='QN'): + values = set() + for event in input_dict['events']: + if event[field] in values: + yield ValidationError(message='Re-used {}'.format(field), event=event) + + +def validate_non_empty_field(input_dict, field='QN'): + for event in input_dict['events']: + if field not in event.keys() or len(event[field]) == 0: + yield ValidationError(message='Empty field {}'.format(field), event=event) + + +def validate_dependent_value(input_dict, key_field, dependent_field): + """ + Validates that two events with the same value in `key_field` always have the + same value in `dependent_field` + """ + value_map = dict() + for event in input_dict['events']: + if key_field not in event.keys(): + continue + + if event[key_field] not in value_map.keys(): + value_map[event[key_field]] = event.get(dependent_field, None) + else: + if value_map[event[key_field]] != event.get(dependent_field, None): + yield ValidationError(message='Field {} depends on key field {} (value={}), expected {}, was {}' + .format(dependent_field, key_field, event[key_field], value_map[key_field], + event.get(dependent_field, None)), event=event) + + + +