Compare commits

...

5 Commits

Author SHA1 Message Date
jamie 9c4f6faf2a Ruff 2026-09-19 15:30:05 -07:00
jamie 754e4d5251 ruffage 2026-09-19 15:20:18 -07:00
jamie 888d9aa5d9 ruff 2026-09-19 14:32:35 -07:00
jamie bd0b09d1db ruff for Reaper script 2026-09-19 14:31:46 -07:00
jamie 3c15237ed2 Renamed method call here for clarity 2026-09-17 21:19:20 -07:00
8 changed files with 79 additions and 83 deletions
+7
View File
@@ -42,6 +42,13 @@ doc = [
"sphinx-rtd-theme>=3.0.2", "sphinx-rtd-theme>=3.0.2",
] ]
[tool.ruff.lint]
ignore = [
"DTZ005" # datetime created without a tz
]
[tool.ruff.lint.per-file-ignores]
"reaper/Export Items as Text.py" = ["F821"]
[project.scripts] [project.scripts]
ptulsconv = "ptulsconv:__main__.main" ptulsconv = "ptulsconv:__main__.main"
+10 -15
View File
@@ -1,20 +1,20 @@
# type: ignore[reportUndefinedVariable]
# Export Items as Text.py # Export Items as Text.py
# (c) 2021 Jamie Hardt. All rights reserved. # (c) 2021 Jamie Hardt. All rights reserved.
# #
# #
import datetime import datetime
import json import json
from zoneinfo import ZoneInfo
item_records = list() item_records = []
for i in range(RPR_CountMediaItems(0)): for i in range(RPR_CountMediaItems(0)):
this_item = RPR_GetMediaItem(0, i) this_item = RPR_GetMediaItem(0, i)
item_record = {} item_record = {}
item_record["mute"] = ( item_record["mute"] = RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0.0
True if RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0.0 else False
)
item_record["duration"] = RPR_GetMediaItemInfo_Value(this_item, "D_LENGTH") item_record["duration"] = RPR_GetMediaItemInfo_Value(this_item, "D_LENGTH")
_, item_record["duration_tc"], _, _, _ = RPR_format_timestr_len( _, item_record["duration_tc"], _, _, _ = RPR_format_timestr_len(
@@ -26,9 +26,7 @@ for i in range(RPR_CountMediaItems(0)):
item_record["position"], "", 128, 5 item_record["position"], "", 128, 5
) )
item_record["selected"] = ( item_record["selected"] = RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0.0
True if RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0.0 else False
)
_, _, _, item_record["notes"], _ = RPR_GetSetMediaItemInfo_String( _, _, _, item_record["notes"], _ = RPR_GetSetMediaItemInfo_String(
this_item, "P_NOTES", "", False this_item, "P_NOTES", "", False
) )
@@ -54,13 +52,11 @@ for i in range(RPR_CountMediaItems(0)):
item_record["track_index"] = RPR_GetMediaTrackInfo_Value( item_record["track_index"] = RPR_GetMediaTrackInfo_Value(
item_track, "IP_TRACKNUMBER" item_track, "IP_TRACKNUMBER"
) )
item_record["track_muted"] = ( item_record["track_muted"] = RPR_GetMediaTrackInfo_Value(item_track, "B_MUTE") > 0.0
True if RPR_GetMediaTrackInfo_Value(item_track, "B_MUTE") > 0.0 else False
)
item_records = item_records + [item_record] item_records = item_records + [item_record]
output = dict() output = {}
output["items"] = item_records output["items"] = item_records
_, output["project_title"], _ = RPR_GetProjectName(0, "", 1024) _, output["project_title"], _ = RPR_GetProjectName(0, "", 1024)
_, _, output["project_author"], _ = RPR_GetSetProjectAuthor(0, False, "", 1024) _, _, output["project_author"], _ = RPR_GetSetProjectAuthor(0, False, "", 1024)
@@ -76,15 +72,14 @@ output_title = output["project_title"]
if output_title == "": if output_title == "":
output_title = "unsaved project" output_title = "unsaved project"
output_file_name = "%s Text Export %s.txt" % (output_title, now.strftime("%Y%m%d_%H%M")) output_file_name = f"{output_title} Text Export {now.strftime('%Y%m%d_%H%M')}.txt"
output_path = output_path + "/" + output_file_name output_path = output_path + "/" + output_file_name
with open(output_path, "w") as f: with open(output_path, "w") as f:
json.dump(output, f, allow_nan=True, indent=4) json.dump(output, f, allow_nan=True, indent=4)
RPR_ShowMessageBox( RPR_ShowMessageBox(
'Exported text file "%s" to project folder.' % output_file_name, f'Exported text file "{output_file_name}" to project folder. Text Export Complete',
"Text Export Complete",
0, 0,
) )
+1 -12
View File
@@ -102,7 +102,7 @@ def main():
(options, args) = parser.parse_args(sys.argv) (options, args) = parser.parse_args(sys.argv)
print_section_header_style("Startup") print_section_header_style("Startup")
print_status_style("This run started %s" % (datetime.datetime.now().isoformat())) print_status_style(f"This run started {datetime.datetime.now().isoformat()}")
if options.show_tags: if options.show_tags:
dump_field_map() dump_field_map()
@@ -124,17 +124,6 @@ def main():
input_file=args[1], major_mode=major_mode, warnings=options.warnings input_file=args[1], major_mode=major_mode, warnings=options.warnings
) )
except FileNotFoundError as e:
print_fatal_error("Error trying to read input file")
raise e
except Exception as e:
import traceback
print_fatal_error("Error trying to convert file")
print("\033[31m" + e.__repr__() + "\033[0m", file=sys.stderr)
print(traceback.format_exc())
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+18 -15
View File
@@ -2,11 +2,13 @@
Useful functions for parsing and working with timecode. Useful functions for parsing and working with timecode.
""" """
from __future__ import annotations
import math import math
import re import re
from collections import namedtuple from collections import namedtuple
from fractions import Fraction from fractions import Fraction
from typing import Optional, SupportsFloat from typing import SupportsFloat
class TimecodeFormat( class TimecodeFormat(
@@ -16,7 +18,7 @@ class TimecodeFormat(
A struct reperesenting a timecode datum. A struct reperesenting a timecode datum.
""" """
def smpte_to_seconds(self, smpte: str) -> Optional[Fraction]: def smpte_to_seconds(self, smpte: str) -> Fraction | None:
frame_count = smpte_to_frame_count( frame_count = smpte_to_frame_count(
smpte, self.logical_fps, drop_frame_hint=self.drop_frame smpte, self.logical_fps, drop_frame_hint=self.drop_frame
) )
@@ -32,7 +34,7 @@ class TimecodeFormat(
def smpte_to_frame_count( def smpte_to_frame_count(
smpte_rep_string: str, frames_per_logical_second: int, drop_frame_hint=False smpte_rep_string: str, frames_per_logical_second: int, drop_frame_hint=False
) -> Optional[int]: ) -> int | None:
""" """
Convert a string with a SMPTE timecode representation into a frame count. Convert a string with a SMPTE timecode representation into a frame count.
@@ -83,7 +85,7 @@ def frame_count_to_smpte(
frame_count: int, frame_count: int,
frames_per_logical_second: int, frames_per_logical_second: int,
drop_frame: bool = False, drop_frame: bool = False,
fractional_frame: Optional[float] = None, fractional_frame: float | None = None,
) -> str: ) -> str:
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60] assert frames_per_logical_second in [24, 25, 30, 48, 50, 60]
assert fractional_frame is None or fractional_frame < 1.0 assert fractional_frame is None or fractional_frame < 1.0
@@ -104,20 +106,21 @@ def frame_count_to_smpte(
ss, ff = divmod(rem, frames_per_logical_second) ss, ff = divmod(rem, frames_per_logical_second)
hh = hh % 24 hh = hh % 24
hh = int(hh)
mm = int(mm)
ss = int(ss)
ff = int(ff)
if fractional_frame is not None and fractional_frame > 0: if fractional_frame is not None and fractional_frame > 0:
return "%02i:%02i:%02i%s%02i%s" % ( fpart = "{fractional_frame:.3f}"[1:]
hh, return f"{hh:02}:{mm:02}:{ss:02}{separator}{ff:02}{fpart}"
mm,
ss,
separator,
ff,
("%.3f" % fractional_frame)[1:],
)
else: else:
return "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff) return f"{hh:02}:{mm:02}:{ss:02}{separator}{ff:02}"
# "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff)
def footage_to_frame_count(footage_string) -> Optional[int]: def footage_to_frame_count(footage_string) -> int | None:
m = re.search(r"(\d+)\+(\d+)(\.\d+)?", footage_string) m = re.search(r"(\d+)\+(\d+)(\.\d+)?", footage_string)
if m is None: if m is None:
return None return None
@@ -131,4 +134,4 @@ def footage_to_frame_count(footage_string) -> Optional[int]:
def frame_count_to_footage(frame_count): def frame_count_to_footage(frame_count):
feet, frm = divmod(frame_count, 16) feet, frm = divmod(frame_count, 16)
return "%i+%02i" % (feet, frm) return f"{feet}+{frm:02}"
+19 -21
View File
@@ -3,6 +3,8 @@ This module provides the main input document parsing and transform
implementation. implementation.
""" """
from __future__ import annotations
import csv import csv
import datetime import datetime
import os import os
@@ -11,7 +13,7 @@ from collections.abc import Iterator
from fractions import Fraction from fractions import Fraction
from itertools import chain from itertools import chain
from json import JSONEncoder from json import JSONEncoder
from typing import List, Optional from zoneinfo import ZoneInfo
import ptsl import ptsl
@@ -39,26 +41,25 @@ class FractionEncoder(JSONEncoder):
as a dict. as a dict.
""" """
force_denominator: Optional[int] force_denominator: int | None
def default(self, o): def default(self, o):
""" """
if isinstance(o, Fraction): if isinstance(o, Fraction):
return dict(numerator=o.numerator, denominator=o.denominator) return {"numerator": o.numerator, "denominator": o.denominator}
else: else:
return o.__dict__ return o.__dict__
def output_adr_csv(lines: List[ADRLine], time_format: TimecodeFormat): def output_adr_csv(lines: list[ADRLine], time_format: TimecodeFormat):
""" """
Writes ADR lines as CSV to the current working directory. Creates Writes ADR lines as CSV to the current working directory. Creates
directories for each character number and name pair, and within that directories for each character number and name pair, and within that
directory, creates a CSV file for each reel. directory, creates a CSV file for each reel.
""" """
reels: set[str | None] = set([ln.reel for ln in lines]) reels: set[str | None] = {ln.reel for ln in lines}
for n, name in [(n.character_id, n.character_name) for n in lines]: for n, name in [(n.character_id, n.character_name) for n in lines]:
dir_name = "%s_%s" % (n, name) dir_name = f"{n}_{name}"
os.makedirs(dir_name, exist_ok=True) os.makedirs(dir_name, exist_ok=True)
os.chdir(dir_name) os.chdir(dir_name)
for reel in reels: for reel in reels:
@@ -69,11 +70,8 @@ def output_adr_csv(lines: List[ADRLine], time_format: TimecodeFormat):
if len(these_lines) == 0: if len(these_lines) == 0:
continue continue
outfile_name = "%s_%s_%s_%s.csv" % ( outfile_name = (
these_lines[0].title, f"{these_lines[0].title}_{n}_{these_lines[0].character_name}_{reel}.csv"
n,
these_lines[0].character_name,
reel,
) )
with open(outfile_name, mode="w", newline="") as outfile: with open(outfile_name, mode="w", newline="") as outfile:
@@ -119,13 +117,13 @@ def output_adr_csv(lines: List[ADRLine], time_format: TimecodeFormat):
os.chdir("..") os.chdir("..")
def generate_documents(session_tc_format, scenes, adr_lines: List[ADRLine], title): def generate_documents(session_tc_format, scenes, adr_lines: list[ADRLine], title):
""" """
Create PDF output. Create PDF output.
""" """
print_section_header_style("Creating PDF Reports") print_section_header_style("Creating PDF Reports")
report_date = datetime.datetime.now() report_date = datetime.datetime.now()
reports_dir = "%s_%s" % (title, report_date.strftime("%Y-%m-%d_%H%M%S")) reports_dir = f"{title}_{report_date.strftime('%Y-%m-%d_%H%M%S')}"
os.makedirs(reports_dir, exist_ok=False) os.makedirs(reports_dir, exist_ok=False)
os.chdir(reports_dir) os.chdir(reports_dir)
@@ -155,7 +153,7 @@ def generate_documents(session_tc_format, scenes, adr_lines: List[ADRLine], titl
def create_adr_reports( def create_adr_reports(
lines: List[ADRLine], tc_display_format: TimecodeFormat, reel_list: List[str] lines: list[ADRLine], tc_display_format: TimecodeFormat, reel_list: list[str]
): ):
""" """
Creates a directory heirarchy and a respective set of ADR reports, Creates a directory heirarchy and a respective set of ADR reports,
@@ -240,18 +238,18 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True):
) )
# TODO: Breakdown by titles # TODO: Breakdown by titles
titles = set([x.title for x in (generic_events + adr_lines)]) titles = {x.title for x in (generic_events + adr_lines)}
if len(titles) != 1: if len(titles) != 1:
print_warning( print_warning(
"Multiple titles per export is not supported, " "Multiple titles per export is not supported, "
"found multiple titles: %s Exiting." % titles f"found multiple titles: {titles} Exiting."
) )
exit(-1) sys.exit(-1)
title = list(titles)[0] title = next(iter(titles))
print_status_style("%i generic events found." % len(generic_events)) print_status_style(f"{len(generic_events)} generic events found.")
print_status_style("%i ADR events found." % len(adr_lines)) print_status_style(f"{len(adr_lines)} ADR events found.")
if warnings: if warnings:
perform_adr_validations(iter(adr_lines)) perform_adr_validations(iter(adr_lines))
+2 -6
View File
@@ -131,7 +131,6 @@ class TagCompiler:
return effective_tags return effective_tags
def parse_data(self) -> Iterator[Intermediate]: def parse_data(self) -> Iterator[Intermediate]:
for track, clip, start, finish, _ in self.session.track_clips_timed(): for track, clip, start, finish, _ in self.session.track_clips_timed():
if clip.state == "Muted": if clip.state == "Muted":
continue continue
@@ -153,7 +152,6 @@ class TagCompiler:
@staticmethod @staticmethod
def apply_appends(parsed: Iterator[Intermediate]) -> Iterator[Intermediate]: def apply_appends(parsed: Iterator[Intermediate]) -> Iterator[Intermediate]:
def should_append(a, b): def should_append(a, b):
return b.clip_tag_mode == TagPreModes.APPEND and b.start >= a.finish return b.clip_tag_mode == TagPreModes.APPEND and b.start >= a.finish
@@ -177,7 +175,6 @@ class TagCompiler:
def collect_time_spans( def collect_time_spans(
parsed: Iterator[Intermediate], parsed: Iterator[Intermediate],
) -> Iterator[tuple[Intermediate, tuple[dict, Fraction, Fraction]]]: ) -> Iterator[tuple[Intermediate, tuple[dict, Fraction, Fraction]]]:
time_spans = [] time_spans = []
for item in parsed: for item in parsed:
@@ -199,7 +196,6 @@ class TagCompiler:
def apply_tags( def apply_tags(
self, parsed_with_time_spans self, parsed_with_time_spans
) -> Iterator[tuple[str, str, str, dict, Fraction, Fraction]]: ) -> Iterator[tuple[str, str, str, dict, Fraction, Fraction]]:
session_parsed = parse_tags(self.session.header.session_name) session_parsed = parse_tags(self.session.header.session_name)
for event, time_spans in parsed_with_time_spans: for event, time_spans in parsed_with_time_spans:
@@ -226,7 +222,7 @@ class TagCompiler:
def apply_appends( def apply_appends(
source: Iterator, should_append: Callable, do_append: Callable source: Iterator, should_append: Callable, preform_append: Callable
) -> Generator: ) -> Generator:
""" """
:param source: :param source:
@@ -240,7 +236,7 @@ def apply_appends(
this_element = next(source) this_element = next(source)
for element in source: for element in source:
if should_append(this_element, element): if should_append(this_element, element):
this_element = do_append(this_element, element) this_element = preform_append(this_element, element)
else: else:
yield this_element yield this_element
this_element = element this_element = element
+2 -2
View File
@@ -1,13 +1,13 @@
""" """
Methods for converting string reprentations of film footage. Methods for converting string reprentations of film footage.
""" """
from __future__ import annotations
import re import re
from fractions import Fraction from fractions import Fraction
from typing import Optional
def footage_to_seconds(footage: str) -> Optional[Fraction]: def footage_to_seconds(footage: str) -> Fraction | None:
""" """
Converts a string representation of a footage (35mm, 24fps) Converts a string representation of a footage (35mm, 24fps)
into a :class:`Fraction`, this fraction being a some number of into a :class:`Fraction`, this fraction being a some number of
+18 -10
View File
@@ -1,11 +1,13 @@
import datetime from __future__ import annotations
import glob import glob
import os import os
import os.path import os.path
import pathlib import pathlib
import subprocess import subprocess
import sys import sys
from typing import List from datetime import datetime
from importlib.metadata import version as module_version
from xml.etree.ElementTree import TreeBuilder, tostring from xml.etree.ElementTree import TreeBuilder, tostring
import ptulsconv import ptulsconv
@@ -15,13 +17,16 @@ from ptulsconv.docparser.adr_entity import ADRLine
def avid_marker_list( def avid_marker_list(
lines: List[ADRLine], lines: list[ADRLine],
report_date=datetime.datetime.now(), report_date: datetime | None = None,
reel_start_frame=0, reel_start_frame=0,
fps=24, fps=24,
): ):
doc = TreeBuilder(element_factory=None) doc = TreeBuilder(element_factory=None)
if report_date is None:
report_date = datetime.now()
doc.start("Avid:StreamItems", {"xmlns:Avid": "http://www.avid.com"}) doc.start("Avid:StreamItems", {"xmlns:Avid": "http://www.avid.com"})
doc.start("Avid:XMLFileData", {}) doc.start("Avid:XMLFileData", {})
doc.start("AvProp", {"name": "DomainMagic", "type": "string"}) doc.start("AvProp", {"name": "DomainMagic", "type": "string"})
@@ -57,6 +62,8 @@ def avid_marker_list(
doc.start("List", {"id": "OMFI:ATTR:AttrRefs"}) doc.start("List", {"id": "OMFI:ATTR:AttrRefs"})
assert report_date
insert_elem( insert_elem(
"1", "1",
"OMFI:ATTB:IntAttribute", "OMFI:ATTB:IntAttribute",
@@ -75,7 +82,7 @@ def avid_marker_list(
line.supervisor or "", line.supervisor or "",
) )
marker_name = "%s: %s" % (line.cue_number, line.prompt) marker_name = f"{line.cue_number}: {line.prompt}"
insert_elem( insert_elem(
"2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_COM", marker_name "2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_COM", marker_name
) )
@@ -112,7 +119,8 @@ def dump_fmpxml(data, input_file_name, output, adr_field_map):
doc.data("0") doc.data("0")
doc.end("ERRORCODE") doc.end("ERRORCODE")
doc.start("PRODUCT", {"NAME": ptulsconv.__name__, "VERSION": ptulsconv.__version__}) version = module_version("ptulsconv")
doc.start("PRODUCT", {"NAME": ptulsconv.__name__, "VERSION": f"{version}"})
doc.end("PRODUCT") doc.end("PRODUCT")
doc.start( doc.start(
@@ -147,7 +155,7 @@ def dump_fmpxml(data, input_file_name, output, adr_field_map):
doc.start("COL", {}) doc.start("COL", {})
doc.start("DATA", {}) doc.start("DATA", {})
for key_attempt in field[0]: for key_attempt in field[0]:
if key_attempt in event.keys(): if key_attempt in event:
doc.data(str(event[key_attempt])) doc.data(str(event[key_attempt]))
break break
doc.end("DATA") doc.end("DATA")
@@ -170,7 +178,7 @@ def xform_options():
def dump_xform_options(output=sys.stdout): def dump_xform_options(output=sys.stdout):
print("# Available transforms:", file=output) print("# Available transforms:", file=output)
print("# Transform dir: %s" % xslt_path, file=output) print(f"# Transform dir: {xslt_path}", file=output)
for f in xform_options(): for f in xform_options():
base = os.path.basename(f) base = os.path.basename(f)
name, _ = os.path.splitext(base) name, _ = os.path.splitext(base)
@@ -188,14 +196,14 @@ def fmp_transformed_dump(data, input_file, xsl_name, output, adr_field_map):
dump_fmpxml(data, input_file, pipe, adr_field_map) dump_fmpxml(data, input_file, pipe, adr_field_map)
str_data = pipe.getvalue() str_data = pipe.getvalue()
print_status_style("Base XML size %i" % (len(str_data))) print_status_style(f"Base XML size {len(str_data)}")
print_status_style("Running xsltproc") print_status_style("Running xsltproc")
xsl_path = os.path.join( xsl_path = os.path.join(
pathlib.Path(__file__).parent.absolute(), "xslt", xsl_name + ".xsl" pathlib.Path(__file__).parent.absolute(), "xslt", xsl_name + ".xsl"
) )
print_status_style("Using xsl: %s" % xsl_path) print_status_style(f"Using xsl: {xsl_path}")
subprocess.run( subprocess.run(
["xsltproc", xsl_path, "-"], ["xsltproc", xsl_path, "-"],
input=str_data, input=str_data,