diff --git a/CHANGELOG.md b/CHANGELOG.md index fa0a75c..18ac897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Support for timecodes whose hours field exceeds 99 hours. Colon-separated formats (SMPTE, SRT, FFMPEG, DLP) now accept an unbounded hours field (still zero-padded to at least 2 digits), so non-strict timecodes such as `100:00:00:00` are parsed correctly. This lifts the previous -99–99 hour range limit; single-digit unpadded hours (`1:00:00:00`) remain invalid + + 支持小时字段超过 99 小时的时码。冒号分隔格式(SMPTE、SRT、FFMPEG、DLP)现在接受无上限的小时字段(仍需补零至至少两位),因此 `100:00:00:00` 等非严格模式时码可被正确解析。此前 -99–99 小时的范围限制已被取消;未补零的单位数小时(`1:00:00:00`)仍视为无效 + ## [1.0.0] - 2026-05-12 ### Summary diff --git a/dftt_timecode/pattern.py b/dftt_timecode/pattern.py index ed5c984..7e841e4 100644 --- a/dftt_timecode/pattern.py +++ b/dftt_timecode/pattern.py @@ -4,11 +4,15 @@ This module defines regex patterns for matching various professional timecode formats used in film, television, and video production. All patterns support negative timecodes with an optional leading minus sign. + +For the colon-separated formats (SMPTE, SRT, FFMPEG, DLP), the hours field must be +zero-padded to at least 2 digits but is otherwise unbounded, so timecodes may exceed +99 hours (e.g. ``100:00:00:00``) as produced in non-strict mode without 24h wraparound. """ import re -SMPTE_NDF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d):){1}(\d?\d\d{1}){1}$') +SMPTE_NDF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d):){1}(\d?\d\d{1}){1}$') """Regex pattern for SMPTE Non-Drop-Frame (NDF) timecode format. Matches: ``HH:MM:SS:FF`` where frames use colon separator. @@ -21,7 +25,7 @@ The colon separator before frames distinguishes NDF from drop-frame format. """ -SMPTE_DF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d);){1}(\d?\d\d{1}){1}$') +SMPTE_DF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d);){1}(\d?\d\d{1}){1}$') """Regex pattern for SMPTE Drop-Frame (DF) timecode format. Matches: ``HH:MM:SS;FF`` where frames use semicolon separator. @@ -35,7 +39,7 @@ Used primarily with 29.97 fps and its multiples (59.94, 119.88). """ -SMPTE_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d);?:?){1}(\d?\d\d{1}){1}$') +SMPTE_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d);?:?){1}(\d?\d\d{1}){1}$') """Regex pattern for any SMPTE timecode format (both NDF and DF). Matches: ``HH:MM:SS:FF`` or ``HH:MM:SS;FF`` @@ -44,7 +48,7 @@ accepting either colon or semicolon before the frame number. """ -SRT_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d),){1}(\d\d\d){1}$') +SRT_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d),){1}(\d\d\d){1}$') """Regex pattern for SubRip (SRT) subtitle timecode format. Matches: ``HH:MM:SS,mmm`` where mmm is milliseconds. @@ -56,7 +60,7 @@ Uses comma separator before milliseconds. This format is frame rate independent. """ -FFMPEG_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d)\.){1}(\d?\d+){1}$') +FFMPEG_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d)\.){1}(\d?\d+){1}$') """Regex pattern for FFmpeg timecode format. Matches: ``HH:MM:SS.ss`` where ss is sub-seconds (centiseconds). @@ -68,7 +72,7 @@ Uses period separator before sub-seconds. Commonly used in video processing tools. """ -DLP_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d):){1}([01][0-9][0-9]|2[0-4][0-9]|25[0]){1}$') +DLP_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d):){1}([01][0-9][0-9]|2[0-4][0-9]|25[0]){1}$') """Regex pattern for DLP Cinema timecode format. Matches: ``HH:MM:SS:sss`` where sss is sub-frames (0-249). diff --git a/docs/.gitignore b/docs/.gitignore index 598c405..cabb8b1 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -7,6 +7,11 @@ __pycache__/ .DS_Store Thumbs.db +# Compiled gettext catalogs — regenerated from .po at build time +# (sphinx-build auto-compiles with gettext_auto_build). Keep .po in git, +# not the binary .mo artifacts. +locale/**/*.mo + # Note: _static/ and _templates/ are NOT ignored # because they contain source files for the documentation # (custom templates and static assets like switcher.json) diff --git a/docs/index.rst b/docs/index.rst index ab847ca..410515e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,7 +22,7 @@ Features - **Multiple Timecode Format Support**: SMPTE (DF/NDF), SRT, DLP (Cine Canvas), FFMPEG, FCPX, frame count, timestamp - **High Frame Rate Support**: Supports frame rates from 0.01 to 999.99 fps - **Drop-Frame/Non-Drop-Frame**: Strictly supports SMPTE DF/NDF formats -- **Extended Time Range**: Currently supports time range from -99 to 99 hours +- **Extended Time Range**: Hours are unbounded (zero-padded to at least 2 digits), so non-strict timecodes may exceed 99 hours (e.g. ``100:00:00:00``) - **Strict Mode**: 24-hour cycling mode that automatically converts timecodes outside the 0-24 hour range - **High Precision**: Internal storage using high-precision Fraction timestamps for accurate conversions - **Rich Operators**: Comprehensive support for arithmetic and comparison operations between timecodes and numbers diff --git a/docs/locale/zh_CN/LC_MESSAGES/README.mo b/docs/locale/zh_CN/LC_MESSAGES/README.mo deleted file mode 100644 index de59310..0000000 Binary files a/docs/locale/zh_CN/LC_MESSAGES/README.mo and /dev/null differ diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/dftt_timecode.mo b/docs/locale/zh_CN/LC_MESSAGES/api/dftt_timecode.mo deleted file mode 100644 index f7243b5..0000000 Binary files a/docs/locale/zh_CN/LC_MESSAGES/api/dftt_timecode.mo and /dev/null differ diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/dftt_timerange.mo b/docs/locale/zh_CN/LC_MESSAGES/api/dftt_timerange.mo deleted file mode 100644 index 85e41a8..0000000 Binary files a/docs/locale/zh_CN/LC_MESSAGES/api/dftt_timerange.mo and /dev/null differ diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/error.mo b/docs/locale/zh_CN/LC_MESSAGES/api/error.mo deleted file mode 100644 index 551d300..0000000 Binary files a/docs/locale/zh_CN/LC_MESSAGES/api/error.mo and /dev/null differ diff --git a/docs/locale/zh_CN/LC_MESSAGES/changelog.mo b/docs/locale/zh_CN/LC_MESSAGES/changelog.mo deleted file mode 100644 index 756bb8b..0000000 Binary files a/docs/locale/zh_CN/LC_MESSAGES/changelog.mo and /dev/null differ diff --git a/docs/locale/zh_CN/LC_MESSAGES/contributing.mo b/docs/locale/zh_CN/LC_MESSAGES/contributing.mo deleted file mode 100644 index 79f8b4b..0000000 Binary files a/docs/locale/zh_CN/LC_MESSAGES/contributing.mo and /dev/null differ diff --git a/docs/locale/zh_CN/LC_MESSAGES/index.mo b/docs/locale/zh_CN/LC_MESSAGES/index.mo deleted file mode 100644 index ebcc54d..0000000 Binary files a/docs/locale/zh_CN/LC_MESSAGES/index.mo and /dev/null differ diff --git a/docs/locale/zh_CN/LC_MESSAGES/index.po b/docs/locale/zh_CN/LC_MESSAGES/index.po index 2dbfabd..f017062 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/index.po +++ b/docs/locale/zh_CN/LC_MESSAGES/index.po @@ -79,9 +79,12 @@ msgstr "**跳帧/非跳帧**:严格支持 SMPTE DF/NDF 格式" #: ../../index.rst:25 msgid "" -"**Extended Time Range**: Currently supports time range from -99 to 99 " -"hours" -msgstr "**扩展时间范围**:目前支持 -99 到 99 小时的时间范围" +"**Extended Time Range**: Hours are unbounded (zero-padded to at least 2 " +"digits), so non-strict timecodes may exceed 99 hours (e.g. " +"``100:00:00:00``)" +msgstr "" +"**扩展时间范围**:小时数无上限(需补零至至少 2 位),因此非严格模式下的时码可超过 99 " +"小时(例如 ``100:00:00:00``)" #: ../../index.rst:26 msgid "" diff --git a/docs/locale/zh_CN/LC_MESSAGES/installation.mo b/docs/locale/zh_CN/LC_MESSAGES/installation.mo deleted file mode 100644 index 8cf69d4..0000000 Binary files a/docs/locale/zh_CN/LC_MESSAGES/installation.mo and /dev/null differ diff --git a/docs/locale/zh_CN/LC_MESSAGES/quickstart.mo b/docs/locale/zh_CN/LC_MESSAGES/quickstart.mo deleted file mode 100644 index 4b02db6..0000000 Binary files a/docs/locale/zh_CN/LC_MESSAGES/quickstart.mo and /dev/null differ diff --git a/docs/locale/zh_CN/LC_MESSAGES/user_guide.mo b/docs/locale/zh_CN/LC_MESSAGES/user_guide.mo deleted file mode 100644 index e7ad508..0000000 Binary files a/docs/locale/zh_CN/LC_MESSAGES/user_guide.mo and /dev/null differ diff --git a/test/test_dftt_timecode.py b/test/test_dftt_timecode.py index 858f58d..9532f29 100644 --- a/test/test_dftt_timecode.py +++ b/test/test_dftt_timecode.py @@ -66,6 +66,45 @@ def test_invalid_timecode(timecode_value, timecode_type, fps, drop_frame, strict TC(timecode_value, timecode_type, fps, drop_frame, strict) +@pytest.mark.parametrize( + "timecode_value, timecode_type, fps, drop_frame, strict, output_type, expected_output", + [ + ("100:00:00:00", "auto", 24, False, False, "smpte", "100:00:00:00"), + ("123:45:59:12", "auto", 24, False, False, "smpte", "123:45:59:12"), + ("1000:00:00:00", "auto", 24, False, False, "smpte", "1000:00:00:00"), + ("100:00:00;00", "auto", 29.97, True, False, "smpte", "100:00:00;00"), + ("100:00:00,000", "srt", 24, False, False, "srt", "100:00:00,000"), + ("1000:00:00,000", "srt", 24, False, False, "srt", "1000:00:00,000"), + ("100:00:00.67", "ffmpeg", 24, False, False, "ffmpeg", "100:00:00.67"), + ("100:00:00:102", "dlp", 24, False, False, "dlp", "100:00:00:102"), + ], + ids=["smpte_ndf", "smpte_ndf_full", "smpte_ndf_4digit", "smpte_df", "srt", "srt_4digit", "ffmpeg", "dlp"], +) +def test_hours_over_two_digits( + timecode_value, timecode_type, fps, drop_frame, strict, output_type, expected_output +): + # Non-strict timecodes may exceed 99h; the hours field must parse and round-trip. + tc = TC(timecode_value, timecode_type, fps, drop_frame, strict) + assert tc.timecode_output(output_type) == expected_output + + +def test_hours_over_two_digits_framecount(): + assert TC("100:00:00:00", "auto", 24, False, False).framecount == 100 * 3600 * 24 + assert TC("1000:00:00:00", "auto", 24, False, False).framecount == 1000 * 3600 * 24 + assert ( + TC("123:45:59:12", "auto", 24, False, False).framecount + == 123 * 3600 * 24 + 45 * 60 * 24 + 59 * 24 + 12 + ) + + +def test_unpadded_single_digit_hours_rejected(): + # Hours must still be zero-padded to at least 2 digits; single-digit is invalid. + from dftt_timecode.error import DFTTTimecodeTypeError + + with pytest.raises(DFTTTimecodeTypeError): + TC("1:00:00:00", "auto", 24, False, False) + + @pytest.mark.parametrize( "timecode_value, timecode_type, fps, drop_frame, strict, result_fps", [