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",
]
[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]
ptulsconv = "ptulsconv:__main__.main"
+10 -15
View File
@@ -1,20 +1,20 @@
# type: ignore[reportUndefinedVariable]
# Export Items as Text.py
# (c) 2021 Jamie Hardt. All rights reserved.
#
#
import datetime
import json
from zoneinfo import ZoneInfo
item_records = list()
item_records = []
for i in range(RPR_CountMediaItems(0)):
this_item = RPR_GetMediaItem(0, i)
item_record = {}
item_record["mute"] = (
True if RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0.0 else False
)
item_record["mute"] = RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0.0
item_record["duration"] = RPR_GetMediaItemInfo_Value(this_item, "D_LENGTH")
_, 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["selected"] = (
True if RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0.0 else False
)
item_record["selected"] = RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0.0
_, _, _, item_record["notes"], _ = RPR_GetSetMediaItemInfo_String(
this_item, "P_NOTES", "", False
)
@@ -54,13 +52,11 @@ for i in range(RPR_CountMediaItems(0)):
item_record["track_index"] = RPR_GetMediaTrackInfo_Value(
item_track, "IP_TRACKNUMBER"
)
item_record["track_muted"] = (
True if RPR_GetMediaTrackInfo_Value(item_track, "B_MUTE") > 0.0 else False
)
item_record["track_muted"] = RPR_GetMediaTrackInfo_Value(item_track, "B_MUTE") > 0.0
item_records = item_records + [item_record]
output = dict()
output = {}
output["items"] = item_records
_, output["project_title"], _ = RPR_GetProjectName(0, "", 1024)
_, _, output["project_author"], _ = RPR_GetSetProjectAuthor(0, False, "", 1024)
@@ -76,15 +72,14 @@ output_title = output["project_title"]
if output_title == "":
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
with open(output_path, "w") as f:
json.dump(output, f, allow_nan=True, indent=4)
RPR_ShowMessageBox(
'Exported text file "%s" to project folder.' % output_file_name,
"Text Export Complete",
f'Exported text file "{output_file_name}" to project folder. Text Export Complete',
0,
)
+1 -12
View File
@@ -102,7 +102,7 @@ def main():
(options, args) = parser.parse_args(sys.argv)
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:
dump_field_map()
@@ -124,17 +124,6 @@ def main():
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__":
main()
+18 -15
View File
@@ -2,11 +2,13 @@
Useful functions for parsing and working with timecode.
"""
from __future__ import annotations
import math
import re
from collections import namedtuple
from fractions import Fraction
from typing import Optional, SupportsFloat
from typing import SupportsFloat
class TimecodeFormat(
@@ -16,7 +18,7 @@ class TimecodeFormat(
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(
smpte, self.logical_fps, drop_frame_hint=self.drop_frame
)
@@ -32,7 +34,7 @@ class TimecodeFormat(
def smpte_to_frame_count(
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.
@@ -83,7 +85,7 @@ def frame_count_to_smpte(
frame_count: int,
frames_per_logical_second: int,
drop_frame: bool = False,
fractional_frame: Optional[float] = None,
fractional_frame: float | None = None,
) -> str:
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60]
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)
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:
return "%02i:%02i:%02i%s%02i%s" % (
hh,
mm,
ss,
separator,
ff,
("%.3f" % fractional_frame)[1:],
)
fpart = "{fractional_frame:.3f}"[1:]
return f"{hh:02}:{mm:02}:{ss:02}{separator}{ff:02}{fpart}"
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)
if m is None:
return None
@@ -131,4 +134,4 @@ def footage_to_frame_count(footage_string) -> Optional[int]:
def frame_count_to_footage(frame_count):
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.
"""
from __future__ import annotations
import csv
import datetime
import os
@@ -11,7 +13,7 @@ from collections.abc import Iterator
from fractions import Fraction
from itertools import chain
from json import JSONEncoder
from typing import List, Optional
from zoneinfo import ZoneInfo
import ptsl
@@ -39,26 +41,25 @@ class FractionEncoder(JSONEncoder):
as a dict.
"""
force_denominator: Optional[int]
force_denominator: int | None
def default(self, o):
""" """
if isinstance(o, Fraction):
return dict(numerator=o.numerator, denominator=o.denominator)
return {"numerator": o.numerator, "denominator": o.denominator}
else:
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
directories for each character number and name pair, and within that
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]:
dir_name = "%s_%s" % (n, name)
dir_name = f"{n}_{name}"
os.makedirs(dir_name, exist_ok=True)
os.chdir(dir_name)
for reel in reels:
@@ -69,11 +70,8 @@ def output_adr_csv(lines: List[ADRLine], time_format: TimecodeFormat):
if len(these_lines) == 0:
continue
outfile_name = "%s_%s_%s_%s.csv" % (
these_lines[0].title,
n,
these_lines[0].character_name,
reel,
outfile_name = (
f"{these_lines[0].title}_{n}_{these_lines[0].character_name}_{reel}.csv"
)
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("..")
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.
"""
print_section_header_style("Creating PDF Reports")
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.chdir(reports_dir)
@@ -155,7 +153,7 @@ def generate_documents(session_tc_format, scenes, adr_lines: List[ADRLine], titl
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,
@@ -240,18 +238,18 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True):
)
# 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:
print_warning(
"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("%i ADR events found." % len(adr_lines))
print_status_style(f"{len(generic_events)} generic events found.")
print_status_style(f"{len(adr_lines)} ADR events found.")
if warnings:
perform_adr_validations(iter(adr_lines))
+4 -8
View File
@@ -131,7 +131,6 @@ class TagCompiler:
return effective_tags
def parse_data(self) -> Iterator[Intermediate]:
for track, clip, start, finish, _ in self.session.track_clips_timed():
if clip.state == "Muted":
continue
@@ -153,7 +152,6 @@ class TagCompiler:
@staticmethod
def apply_appends(parsed: Iterator[Intermediate]) -> Iterator[Intermediate]:
def should_append(a, b):
return b.clip_tag_mode == TagPreModes.APPEND and b.start >= a.finish
@@ -177,7 +175,6 @@ class TagCompiler:
def collect_time_spans(
parsed: Iterator[Intermediate],
) -> Iterator[tuple[Intermediate, tuple[dict, Fraction, Fraction]]]:
time_spans = []
for item in parsed:
@@ -199,7 +196,6 @@ class TagCompiler:
def apply_tags(
self, parsed_with_time_spans
) -> Iterator[tuple[str, str, str, dict, Fraction, Fraction]]:
session_parsed = parse_tags(self.session.header.session_name)
for event, time_spans in parsed_with_time_spans:
@@ -226,13 +222,13 @@ class TagCompiler:
def apply_appends(
source: Iterator, should_append: Callable, do_append: Callable
source: Iterator, should_append: Callable, preform_append: Callable
) -> Generator:
"""
:param source:
:param should_append: Called with two variables a and b, your
function should return true if b should be
appended to a
function should return true if b should be
appended to a
:param do_append: Called with two variables a and b, your function
should return
:returns: A Generator
@@ -240,7 +236,7 @@ def apply_appends(
this_element = next(source)
for element in source:
if should_append(this_element, element):
this_element = do_append(this_element, element)
this_element = preform_append(this_element, element)
else:
yield this_element
this_element = element
+2 -2
View File
@@ -1,13 +1,13 @@
"""
Methods for converting string reprentations of film footage.
"""
from __future__ import annotations
import re
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)
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 os
import os.path
import pathlib
import subprocess
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
import ptulsconv
@@ -15,13 +17,16 @@ from ptulsconv.docparser.adr_entity import ADRLine
def avid_marker_list(
lines: List[ADRLine],
report_date=datetime.datetime.now(),
lines: list[ADRLine],
report_date: datetime | None = None,
reel_start_frame=0,
fps=24,
):
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:XMLFileData", {})
doc.start("AvProp", {"name": "DomainMagic", "type": "string"})
@@ -57,6 +62,8 @@ def avid_marker_list(
doc.start("List", {"id": "OMFI:ATTR:AttrRefs"})
assert report_date
insert_elem(
"1",
"OMFI:ATTB:IntAttribute",
@@ -75,7 +82,7 @@ def avid_marker_list(
line.supervisor or "",
)
marker_name = "%s: %s" % (line.cue_number, line.prompt)
marker_name = f"{line.cue_number}: {line.prompt}"
insert_elem(
"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.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.start(
@@ -147,7 +155,7 @@ def dump_fmpxml(data, input_file_name, output, adr_field_map):
doc.start("COL", {})
doc.start("DATA", {})
for key_attempt in field[0]:
if key_attempt in event.keys():
if key_attempt in event:
doc.data(str(event[key_attempt]))
break
doc.end("DATA")
@@ -170,7 +178,7 @@ def xform_options():
def dump_xform_options(output=sys.stdout):
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():
base = os.path.basename(f)
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)
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")
xsl_path = os.path.join(
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(
["xsltproc", xsl_path, "-"],
input=str_data,