15 | {% if language == 'en' %}
16 | English
17 | {% else %}
18 | {# From zh_CN to en: go up one level #}
19 | English
20 | {% endif %}
21 |
22 |
23 | {% if language == 'zh-CN' or language == 'zh_CN' %}
24 | 中文 (简体)
25 | {% else %}
26 | {# From en to zh_CN: go into zh_CN subdirectory #}
27 | 中文 (简体)
28 | {% endif %}
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/docs/api/dftt_timerange.rst:
--------------------------------------------------------------------------------
1 | DfttTimeRange API
2 | =================
3 |
4 | .. currentmodule:: dftt_timecode.core.dftt_timerange
5 |
6 | .. autoclass:: DfttTimeRange
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 | :special-members: __init__, __str__, __repr__, __contains__, __eq__, __ne__
11 |
12 | Core Class
13 | ----------
14 |
15 | The :class:`DfttTimeRange` class represents a time range with start and end points, built on top of :class:`DfttTimecode`.
16 |
17 | Examples
18 | --------
19 |
20 | Basic Usage
21 | ~~~~~~~~~~~
22 |
23 | .. code-block:: python
24 |
25 | from dftt_timecode import DfttTimecode, DfttTimeRange
26 |
27 | # Create time range
28 | start = DfttTimecode('01:00:00:00', 'auto', fps=24)
29 | end = DfttTimecode('02:00:00:00', 'auto', fps=24)
30 | range1 = DfttTimeRange(start, end)
31 |
32 | # Get duration
33 | print(range1.duration.timecode_output('smpte')) # '01:00:00:00'
34 |
35 | Checking Containment and Intersection
36 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
37 |
38 | .. code-block:: python
39 |
40 | tc = DfttTimecode('01:30:00:00', 'auto', fps=24)
41 |
42 | # Check if timecode is within range
43 | if tc in range1:
44 | print("Timecode is within range")
45 |
46 | # Create another range
47 | range2 = DfttTimeRange(
48 | DfttTimecode('01:30:00:00', 'auto', fps=24),
49 | DfttTimecode('02:30:00:00', 'auto', fps=24)
50 | )
51 |
52 | # Get intersection of two ranges
53 | intersection = range1.intersect(range2)
54 | print(intersection)
55 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "dftt-timecode"
7 | version = "1.0.0b3"
8 | description = "Timecode library for film and TV industry, supports HFR and a bunch of cool features"
9 | readme = "README.md"
10 | requires-python = ">=3.11"
11 | license = "LGPL-2.1"
12 | authors = [
13 | {name = "You Ziyuan", email = "hikaridragon0216@gmail.com"},
14 | {name = "Wheheo Hu", email = "wheheohu@outlook.com"}
15 | ]
16 | classifiers = [
17 | "Programming Language :: Python :: 3",
18 | "Programming Language :: Python :: 3.11",
19 | "Programming Language :: Python :: 3.12",
20 | "Development Status :: 4 - Beta",
21 | "Natural Language :: Chinese (Simplified)",
22 | "Operating System :: OS Independent",
23 | ]
24 | keywords = ["timecode", "video", "film", "television", "smpte", "hfr"]
25 |
26 | dependencies = []
27 |
28 | [project.urls]
29 | Homepage = "https://github.com/OwenYou/dftt_timecode"
30 | Documentation = "https://owenyou.github.io/dftt_timecode/"
31 | Repository = "https://github.com/OwenYou/dftt_timecode"
32 | Issues = "https://github.com/OwenYou/dftt_timecode/issues"
33 |
34 | [tool.setuptools.packages.find]
35 | include = ["dftt_timecode*"]
36 |
37 | [tool.pytest.ini_options]
38 | addopts = "-v -s"
39 | testpaths = ["test"]
40 |
41 | [dependency-groups]
42 | dev = [
43 | "pytest>=8.3.4",
44 | "sphinx>=7.0.0",
45 | "pydata-sphinx-theme>=0.15.0",
46 | "myst-parser>=4.0.0",
47 | "sphinx-intl>=2.0.0",
48 | ]
49 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Build and Deploy Documentation
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | permissions:
12 | contents: read
13 | pages: write
14 | id-token: write
15 |
16 | # Allow one concurrent deployment
17 | concurrency:
18 | group: "pages"
19 | cancel-in-progress: true
20 |
21 | jobs:
22 | build:
23 | runs-on: macos-latest
24 |
25 | steps:
26 | - name: Checkout repository
27 | uses: actions/checkout@v4
28 |
29 | - name: Set up Python
30 | uses: actions/setup-python@v5
31 | with:
32 | python-version: '3.11'
33 |
34 | - name: Install uv
35 | uses: astral-sh/setup-uv@v5
36 | with:
37 | enable-cache: true
38 |
39 | - name: Install dependencies
40 | run: uv sync
41 |
42 | - name: Build documentation with make
43 | run: |
44 | cd docs
45 | uv run make html-all
46 |
47 | - name: Upload artifact
48 | uses: actions/upload-pages-artifact@v3
49 | with:
50 | path: docs/_build/html
51 |
52 | deploy:
53 | # Only deploy on pushes to main
54 | if: github.event_name == 'push' && github.ref == 'refs/heads/main'
55 |
56 | environment:
57 | name: github-pages
58 | url: ${{ steps.deployment.outputs.page_url }}
59 |
60 | runs-on: ubuntu-latest
61 | needs: build
62 |
63 | steps:
64 | - name: Deploy to GitHub Pages
65 | id: deployment
66 | uses: actions/deploy-pages@v4
67 |
68 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | This directory contains the Sphinx documentation for dftt_timecode.
4 |
5 | ## Building Documentation Locally
6 |
7 | ### Prerequisites
8 |
9 | Install the required dependencies:
10 |
11 | ```bash
12 | pip install sphinx pydata-sphinx-theme
13 | ```
14 |
15 | ### Build HTML Documentation
16 |
17 | On Unix/macOS:
18 |
19 | ```bash
20 | cd docs
21 | make html
22 | ```
23 |
24 | On Windows:
25 |
26 | ```bash
27 | cd docs
28 | make.bat html
29 | ```
30 |
31 | The built documentation will be available in `docs/_build/html/`.
32 |
33 | ### View Documentation
34 |
35 | Open `docs/_build/html/index.html` in your web browser.
36 |
37 | ### Other Build Formats
38 |
39 | ```bash
40 | make latexpdf # Build PDF (requires LaTeX)
41 | make epub # Build EPUB
42 | make help # See all available formats
43 | ```
44 |
45 | ## Automatic Deployment
46 |
47 | Documentation is automatically built and deployed to GitHub Pages when changes are pushed to the main branch.
48 |
49 | The deployment is handled by the GitHub Actions workflow in `.github/workflows/docs.yml`.
50 |
51 | ## Documentation Structure
52 |
53 | - `index.rst` - Home page
54 | - `installation.rst` - Installation instructions
55 | - `quickstart.rst` - Quick start guide
56 | - `user_guide.rst` - Comprehensive user guide
57 | - `api/` - API reference documentation
58 | - `contributing.rst` - Contributing guidelines
59 | - `changelog.rst` - Version history
60 | - `conf.py` - Sphinx configuration
61 | - `_static/` - Static files (CSS, images, etc.)
62 | - `_templates/` - Custom templates
63 |
64 | ## Writing Documentation
65 |
66 | - Documentation is written in reStructuredText (RST) format
67 | - API documentation is auto-generated from docstrings using Sphinx autodoc
68 | - Follow the existing style and structure when adding new pages
69 | - Build and preview locally before committing changes
70 |
--------------------------------------------------------------------------------
/docs/api/dftt_timecode.rst:
--------------------------------------------------------------------------------
1 | DfttTimecode API
2 | ================
3 |
4 | .. currentmodule:: dftt_timecode.core.dftt_timecode
5 |
6 | .. autoclass:: DfttTimecode
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 | :special-members: __init__, __str__, __repr__, __add__, __sub__, __mul__, __truediv__, __neg__, __eq__, __ne__, __lt__, __le__, __gt__, __ge__, __int__, __float__
11 |
12 | Core Class
13 | ----------
14 |
15 | The :class:`DfttTimecode` class is the main interface for working with timecodes in various formats.
16 |
17 | Supported Timecode Types
18 | -------------------------
19 |
20 | The following timecode types are supported:
21 |
22 | - **auto**: Automatic detection based on input format
23 | - **smpte**: SMPTE timecode format (HH:MM:SS:FF or HH:MM:SS;FF for drop-frame)
24 | - **srt**: SubRip subtitle format (HH:MM:SS,mmm)
25 | - **ffmpeg**: FFmpeg format (HH:MM:SS.ff)
26 | - **fcpx**: Final Cut Pro X format (frames/fps)
27 | - **dlp**: DLP Cinema format (HH:MM:SS:FFF)
28 | - **frame**: Frame count (integer)
29 | - **time**: Timestamp in seconds (float)
30 |
31 | Examples
32 | --------
33 |
34 | Basic Usage
35 | ~~~~~~~~~~~
36 |
37 | .. code-block:: python
38 |
39 | from dftt_timecode import DfttTimecode
40 |
41 | # Create a timecode
42 | tc = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True)
43 |
44 | # Access properties
45 | print(tc.framecount) # 86400
46 | print(tc.timestamp) # 3600.0
47 |
48 | Format Conversion
49 | ~~~~~~~~~~~~~~~~~
50 |
51 | .. code-block:: python
52 |
53 | tc = DfttTimecode('01:00:00:00', 'auto', fps=24)
54 |
55 | # Convert to different formats
56 | print(tc.timecode_output('srt')) # '01:00:00,000'
57 | print(tc.timecode_output('ffmpeg')) # '01:00:00.00'
58 |
59 | Arithmetic Operations
60 | ~~~~~~~~~~~~~~~~~~~~~
61 |
62 | .. code-block:: python
63 |
64 | tc1 = DfttTimecode('01:00:00:00', 'auto', fps=24)
65 | tc2 = DfttTimecode('00:30:00:00', 'auto', fps=24)
66 |
67 | # Add timecodes
68 | result = tc1 + tc2
69 | print(result.timecode_output('smpte')) # '01:30:00:00'
70 |
71 | # Multiply by factor
72 | result = tc1 * 2
73 | print(result.timecode_output('smpte')) # '02:00:00:00'
74 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # i18n settings
12 | LANGUAGES = en zh_CN
13 |
14 | # Put it first so that "make" without argument is like "make help".
15 | help:
16 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
17 | @echo ""
18 | @echo "i18n targets:"
19 | @echo " gettext to generate .pot files for translation"
20 | @echo " update-po to update .po files from .pot files"
21 | @echo " html-all to build HTML documentation for all languages"
22 | @echo " html-zh to build HTML documentation for Chinese only"
23 |
24 | .PHONY: help Makefile gettext update-po html-all html-zh
25 |
26 | # Generate .pot files for translation
27 | gettext:
28 | @$(SPHINXBUILD) -b gettext "$(SOURCEDIR)" "$(BUILDDIR)/gettext" $(SPHINXOPTS) $(O)
29 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/gettext."
30 |
31 | # Update .po files from .pot files
32 | update-po: gettext
33 | sphinx-intl update -p "$(BUILDDIR)/gettext" -l zh_CN
34 | @echo "Update finished. Translation files are in locale/zh_CN/LC_MESSAGES/."
35 |
36 | # Build HTML documentation for all languages
37 | html-all:
38 | @echo "Building English documentation..."
39 | @$(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O)
40 | @echo "Building Chinese documentation..."
41 | @$(SPHINXBUILD) -b html -D language=zh_CN "$(SOURCEDIR)" "$(BUILDDIR)/html/zh_CN" $(SPHINXOPTS) $(O)
42 | @echo ""
43 | @echo "Build finished. HTML documentation for all languages is in $(BUILDDIR)/html/."
44 |
45 | # Build HTML documentation for Chinese only
46 | html-zh:
47 | @echo "Building Chinese documentation..."
48 | @$(SPHINXBUILD) -b html -D language=zh_CN "$(SOURCEDIR)" "$(BUILDDIR)/html/zh_CN" $(SPHINXOPTS) $(O)
49 | @echo ""
50 | @echo "Build finished. Chinese HTML documentation is in $(BUILDDIR)/html/zh_CN/."
51 |
52 | # Catch-all target: route all unknown targets to Sphinx using the new
53 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
54 | %: Makefile
55 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
56 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | Requirements
5 | ------------
6 |
7 | - Python >= 3.11
8 | - Standard library dependencies only (no external dependencies required)
9 |
10 | - fractions
11 | - logging
12 | - math
13 | - functools
14 | - re
15 |
16 | Installation from PyPI
17 | ----------------------
18 |
19 | The easiest way to install dftt_timecode is using pip or uv:
20 |
21 | .. code-block:: bash
22 |
23 | # Using pip
24 | pip install dftt_timecode
25 |
26 | # Using uv (recommended)
27 | uv pip install dftt_timecode
28 |
29 | Installation from Source
30 | -------------------------
31 |
32 | For development, we recommend using **uv** for faster and more reliable dependency management:
33 |
34 | .. code-block:: bash
35 |
36 | # Clone the repository
37 | git clone https://github.com/OwenYou/dftt_timecode.git
38 | cd dftt_timecode
39 |
40 | # Install uv if you haven't already
41 | curl -LsSf https://astral.sh/uv/install.sh | sh
42 |
43 | # Sync dependencies and install in development mode
44 | uv sync
45 |
46 | This will:
47 |
48 | - Create a virtual environment in ``.venv``
49 | - Install all development dependencies (pytest, sphinx, etc.)
50 | - Install the package in editable mode
51 |
52 | Alternatively, using pip:
53 |
54 | .. code-block:: bash
55 |
56 | git clone https://github.com/OwenYou/dftt_timecode.git
57 | cd dftt_timecode
58 | pip install -e .
59 |
60 | Verifying Installation
61 | -----------------------
62 |
63 | You can verify the installation by importing the package:
64 |
65 | .. code-block:: python
66 |
67 | import dftt_timecode
68 | print(dftt_timecode.__version__)
69 |
70 | Development Dependencies
71 | ------------------------
72 |
73 | The project uses **uv** for dependency management. All dependencies are defined in ``pyproject.toml``:
74 |
75 | - **pytest** - Testing framework
76 | - **sphinx** - Documentation generator
77 | - **pydata-sphinx-theme** - Documentation theme
78 |
79 | To install development dependencies with uv:
80 |
81 | .. code-block:: bash
82 |
83 | # Install all development dependencies
84 | uv sync
85 |
86 | # Activate the virtual environment
87 | source .venv/bin/activate # On Windows: .venv\Scripts\activate
88 |
89 | To run tests:
90 |
91 | .. code-block:: bash
92 |
93 | pytest
94 |
95 | To build documentation:
96 |
97 | .. code-block:: bash
98 |
99 | cd docs
100 | make html
101 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | dftt_timecode
2 | =============
3 |
4 | .. image:: https://img.shields.io/badge/pypi-0.0.14-brightgreen
5 | :target: https://pypi.org/project/dftt-timecode/
6 | :alt: PyPI
7 |
8 | .. image:: https://img.shields.io/badge/python-3-blue
9 | :alt: Python 3
10 |
11 | .. image:: https://img.shields.io/badge/license-LGPL2.1-green
12 | :target: https://github.com/OwenYou/dftt_timecode/blob/main/LICENSE
13 | :alt: License
14 |
15 | Python timecode library for film and TV industry, with high frame rate support and comprehensive features.
16 |
17 | DFTT stands for the Department of Film and TV Technology of Beijing Film Academy.
18 |
19 | Features
20 | --------
21 |
22 | - **Multiple Timecode Format Support**: SMPTE (DF/NDF), SRT, DLP (Cine Canvas), FFMPEG, FCPX, frame count, timestamp
23 | - **High Frame Rate Support**: Supports frame rates from 0.01 to 999.99 fps
24 | - **Drop-Frame/Non-Drop-Frame**: Strictly supports SMPTE DF/NDF formats
25 | - **Extended Time Range**: Currently supports time range from -99 to 99 hours
26 | - **Strict Mode**: 24-hour cycling mode that automatically converts timecodes outside the 0-24 hour range
27 | - **High Precision**: Internal storage using high-precision Fraction timestamps for accurate conversions
28 | - **Rich Operators**: Comprehensive support for arithmetic and comparison operations between timecodes and numbers
29 |
30 | Installation
31 | ------------
32 |
33 | .. code-block:: bash
34 |
35 | pip install dftt_timecode
36 |
37 | Quick Start
38 | -----------
39 |
40 | .. code-block:: python
41 |
42 | from dftt_timecode import DfttTimecode
43 |
44 | # Create a timecode object
45 | tc = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True)
46 |
47 | # Access properties
48 | print(tc.type) # 'smpte'
49 | print(tc.fps) # 24
50 | print(tc.framecount) # 86400
51 | print(tc.timestamp) # 3600.0
52 |
53 | # Convert between formats
54 | print(tc.timecode_output('srt')) # '01:00:00,000'
55 | print(tc.timecode_output('ffmpeg')) # '01:00:00.00'
56 |
57 | # Arithmetic operations
58 | tc2 = DfttTimecode('00:30:00:00', 'auto', fps=24)
59 | result = tc + tc2
60 | print(result.timecode_output('smpte')) # '01:30:00:00'
61 |
62 | # Comparison operations
63 | print(tc > tc2) # True
64 |
65 | Contents
66 | --------
67 |
68 | .. toctree::
69 | :maxdepth: 2
70 | :caption: User Guide
71 |
72 | installation
73 | quickstart
74 | user_guide
75 |
76 | .. toctree::
77 | :maxdepth: 2
78 | :caption: API Reference
79 |
80 | api/dftt_timecode
81 | api/dftt_timerange
82 | api/error
83 |
84 | .. toctree::
85 | :maxdepth: 1
86 | :caption: Development
87 |
88 | contributing
89 | changelog
90 |
91 | Indices and tables
92 | ==================
93 |
94 | * :ref:`genindex`
95 | * :ref:`modindex`
96 | * :ref:`search`
97 |
--------------------------------------------------------------------------------
/docs/locale/zh_CN/LC_MESSAGES/quickstart.po:
--------------------------------------------------------------------------------
1 | # DFTT Timecode Documentation Chinese Translation
2 | # Copyright (C) 2025, You Ziyuan
3 | # This file is distributed under the same license as the DFTT Timecode
4 | # package.
5 | # You Ziyuan , 2025.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: DFTT Timecode 0.0.15a2\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2025-10-21 11:37+0800\n"
12 | "PO-Revision-Date: 2025-10-21 12:05+0800\n"
13 | "Last-Translator: You Ziyuan \n"
14 | "Language: zh_CN\n"
15 | "Language-Team: zh_CN \n"
16 | "Plural-Forms: nplurals=1; plural=0;\n"
17 | "MIME-Version: 1.0\n"
18 | "Content-Type: text/plain; charset=utf-8\n"
19 | "Content-Transfer-Encoding: 8bit\n"
20 | "Generated-By: Babel 2.17.0\n"
21 |
22 | #: ../../quickstart.rst:2
23 | msgid "Quick Start"
24 | msgstr "快速开始"
25 |
26 | #: ../../quickstart.rst:4
27 | msgid "This guide will help you get started with dftt_timecode quickly."
28 | msgstr "本指南将帮助您快速上手 dftt_timecode。"
29 |
30 | #: ../../quickstart.rst:7
31 | msgid "Importing the Library"
32 | msgstr "导入库"
33 |
34 | #: ../../quickstart.rst:14
35 | msgid "Creating Timecode Objects"
36 | msgstr "创建时码对象"
37 |
38 | #: ../../quickstart.rst:16
39 | msgid "There are multiple ways to create timecode objects:"
40 | msgstr "创建时码对象有多种方式:"
41 |
42 | #: ../../quickstart.rst:19
43 | msgid "SMPTE Format"
44 | msgstr "SMPTE 格式"
45 |
46 | #: ../../quickstart.rst:30
47 | msgid "Frame Count"
48 | msgstr "帧计数"
49 |
50 | #: ../../quickstart.rst:41
51 | msgid "Timestamp"
52 | msgstr "时间戳"
53 |
54 | #: ../../quickstart.rst:56
55 | msgid "Other Formats"
56 | msgstr "其他格式"
57 |
58 | #: ../../quickstart.rst:70
59 | msgid "Accessing Timecode Properties"
60 | msgstr "访问时码属性"
61 |
62 | #: ../../quickstart.rst:84
63 | msgid "Converting Between Formats"
64 | msgstr "格式转换"
65 |
66 | #: ../../quickstart.rst:101
67 | msgid "Arithmetic Operations"
68 | msgstr "算术运算"
69 |
70 | #: ../../quickstart.rst:104
71 | msgid "Adding Timecodes"
72 | msgstr "时码相加"
73 |
74 | #: ../../quickstart.rst:123
75 | msgid "Subtracting Timecodes"
76 | msgstr "时码相减"
77 |
78 | #: ../../quickstart.rst:134
79 | msgid "Multiplying and Dividing"
80 | msgstr "乘法和除法"
81 |
82 | #: ../../quickstart.rst:149
83 | msgid "Comparison Operations"
84 | msgstr "比较运算"
85 |
86 | #: ../../quickstart.rst:164
87 | msgid "Changing Timecode Properties"
88 | msgstr "修改时码属性"
89 |
90 | #: ../../quickstart.rst:167
91 | msgid "Changing Frame Rate"
92 | msgstr "更改帧率"
93 |
94 | #: ../../quickstart.rst:182
95 | msgid "Changing Strict Mode"
96 | msgstr "更改严格模式"
97 |
98 | #: ../../quickstart.rst:193
99 | msgid "Changing Timecode Type"
100 | msgstr "更改时码类型"
101 |
102 | #: ../../quickstart.rst:205
103 | msgid "Strict Mode"
104 | msgstr "严格模式"
105 |
106 | #: ../../quickstart.rst:207
107 | msgid "Strict mode ensures timecodes stay within a 24-hour range:"
108 | msgstr "严格模式确保时码保持在 24 小时范围内:"
109 |
--------------------------------------------------------------------------------
/dftt_timecode/error.py:
--------------------------------------------------------------------------------
1 | """
2 | Custom exceptions for DFTT Timecode library.
3 |
4 | This module defines all custom exception classes used throughout the DFTT Timecode library
5 | for handling timecode-specific errors and timerange-specific errors.
6 | """
7 |
8 |
9 | class DFTTError(Exception):
10 | """Base exception class for all DFTT Timecode library errors.
11 |
12 | All custom exceptions in this library inherit from this base class.
13 | """
14 | pass
15 |
16 |
17 | class DFTTTimecodeValueError(DFTTError):
18 | """Raised when a timecode value is invalid or out of acceptable range.
19 |
20 | Examples include:
21 | - Frame number exceeding the frame rate limit
22 | - Invalid drop-frame timecode values
23 | - Illegal timecode values for the given parameters
24 | """
25 | pass
26 |
27 |
28 | class DFTTTimecodeInitializationError(DFTTError):
29 | """Raised when timecode initialization fails due to incompatible parameters.
30 |
31 | Examples include:
32 | - Drop-frame status mismatch with timecode format
33 | - Incompatible timecode value and type combinations
34 | """
35 | pass
36 |
37 |
38 | class DFTTTimecodeTypeError(DFTTError):
39 | """Raised when a timecode type is invalid or incompatible.
40 |
41 | Examples include:
42 | - Unknown timecode format type
43 | - Type mismatch between expected and actual timecode format
44 | - Invalid data type for timecode operations
45 | """
46 | pass
47 |
48 |
49 | class DFTTTimecodeOperatorError(DFTTError):
50 | """Raised when an arithmetic or comparison operation on timecode objects fails.
51 |
52 | Examples include:
53 | - Operations between timecodes with different frame rates
54 | - Undefined operations (e.g., dividing number by timecode)
55 | - Invalid operand types for timecode arithmetic
56 | """
57 | pass
58 |
59 |
60 | class DFTTTimeRangeMethodError(DFTTError):
61 | """Raised when a timerange method is called with invalid parameters or conditions.
62 |
63 | Examples include:
64 | - Attempting to intersect/union timeranges with different directions
65 | - Invalid offset or extend values
66 | - Operations on non-overlapping, non-adjacent timeranges
67 | """
68 | pass
69 |
70 |
71 | class DFTTTimeRangeValueError(DFTTError):
72 | """Raised when a timerange value is invalid or out of acceptable range.
73 |
74 | Examples include:
75 | - Zero-length timerange
76 | - Duration exceeding 24 hours in strict mode
77 | - Invalid timerange separation parameters
78 | """
79 | pass
80 |
81 |
82 | class DFTTTimeRangeTypeError(DFTTError):
83 | """Raised when a timerange type or operand type is invalid.
84 |
85 | Examples include:
86 | - Invalid item type for contains check
87 | - Attempting to operate on non-timerange objects
88 | - Type mismatches in timerange operations
89 | """
90 | pass
91 |
92 |
93 | class DFTTTimeRangeFPSError(DFTTError):
94 | """Raised when timerange operations fail due to frame rate mismatches.
95 |
96 | Examples include:
97 | - FPS mismatch between start and end timecodes
98 | - Operations between timeranges with different frame rates
99 | """
100 | pass
101 |
102 |
--------------------------------------------------------------------------------
/docs/locale/zh_CN/LC_MESSAGES/index.po:
--------------------------------------------------------------------------------
1 | # DFTT Timecode Documentation Chinese Translation
2 | # Copyright (C) 2025, You Ziyuan
3 | # This file is distributed under the same license as the DFTT Timecode
4 | # package.
5 | # You Ziyuan , 2025.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: DFTT Timecode 0.0.15a2\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2025-10-21 13:28+0800\n"
12 | "PO-Revision-Date: 2025-10-21 11:40+0800\n"
13 | "Last-Translator: You Ziyuan \n"
14 | "Language: zh_CN\n"
15 | "Language-Team: zh_CN \n"
16 | "Plural-Forms: nplurals=1; plural=0;\n"
17 | "MIME-Version: 1.0\n"
18 | "Content-Type: text/plain; charset=utf-8\n"
19 | "Content-Transfer-Encoding: 8bit\n"
20 | "Generated-By: Babel 2.17.0\n"
21 |
22 | #: ../../index.rst:68
23 | msgid "User Guide"
24 | msgstr "用户指南"
25 |
26 | #: ../../index.rst:76
27 | msgid "API Reference"
28 | msgstr "API 参考"
29 |
30 | #: ../../index.rst:84
31 | msgid "Development"
32 | msgstr "开发"
33 |
34 | #: ../../index.rst:2
35 | msgid "dftt_timecode"
36 | msgstr "dftt_timecode"
37 |
38 | #: ../../index.rst:4
39 | msgid "PyPI"
40 | msgstr "PyPI"
41 |
42 | #: ../../index.rst:8
43 | msgid "Python 3"
44 | msgstr "Python 3"
45 |
46 | #: ../../index.rst:11
47 | msgid "License"
48 | msgstr "许可证"
49 |
50 | #: ../../index.rst:15
51 | msgid ""
52 | "Python timecode library for film and TV industry, with high frame rate "
53 | "support and comprehensive features."
54 | msgstr "为影视行业设计的Python时码库,支持HFR高帧率以及其他丰富的功能。"
55 |
56 | #: ../../index.rst:17
57 | msgid ""
58 | "DFTT stands for the Department of Film and TV Technology of Beijing Film "
59 | "Academy."
60 | msgstr "DFTT 代表北京电影学院电影电视技术系。"
61 |
62 | #: ../../index.rst:20
63 | msgid "Features"
64 | msgstr "特性"
65 |
66 | #: ../../index.rst:22
67 | msgid ""
68 | "**Multiple Timecode Format Support**: SMPTE (DF/NDF), SRT, DLP (Cine "
69 | "Canvas), FFMPEG, FCPX, frame count, timestamp"
70 | msgstr "**多种时码格式支持**:SMPTE (DF/NDF)、SRT、DLP (Cine Canvas)、FFMPEG、FCPX、帧计数、时间戳"
71 |
72 | #: ../../index.rst:23
73 | msgid "**High Frame Rate Support**: Supports frame rates from 0.01 to 999.99 fps"
74 | msgstr "**高帧率支持**:支持 0.01 到 999.99 fps 的帧率"
75 |
76 | #: ../../index.rst:24
77 | msgid "**Drop-Frame/Non-Drop-Frame**: Strictly supports SMPTE DF/NDF formats"
78 | msgstr "**跳帧/非跳帧**:严格支持 SMPTE DF/NDF 格式"
79 |
80 | #: ../../index.rst:25
81 | msgid ""
82 | "**Extended Time Range**: Currently supports time range from -99 to 99 "
83 | "hours"
84 | msgstr "**扩展时间范围**:目前支持 -99 到 99 小时的时间范围"
85 |
86 | #: ../../index.rst:26
87 | msgid ""
88 | "**Strict Mode**: 24-hour cycling mode that automatically converts "
89 | "timecodes outside the 0-24 hour range"
90 | msgstr "**严格模式**:24 小时循环模式,自动转换 0-24 小时范围外的时码"
91 |
92 | #: ../../index.rst:27
93 | msgid ""
94 | "**High Precision**: Internal storage using high-precision Fraction "
95 | "timestamps for accurate conversions"
96 | msgstr "**高精度**:内部使用高精度分数时间戳进行精确转换"
97 |
98 | #: ../../index.rst:28
99 | msgid ""
100 | "**Rich Operators**: Comprehensive support for arithmetic and comparison "
101 | "operations between timecodes and numbers"
102 | msgstr "**丰富的运算符**:全面支持时码与数字之间的算术和比较运算"
103 |
104 | #: ../../index.rst:31
105 | msgid "Installation"
106 | msgstr "安装"
107 |
108 | #: ../../index.rst:38
109 | msgid "Quick Start"
110 | msgstr "快速开始"
111 |
112 | #: ../../index.rst:66
113 | msgid "Contents"
114 | msgstr "目录"
115 |
116 | #: ../../index.rst:92
117 | msgid "Indices and tables"
118 | msgstr "索引和表格"
119 |
120 | #: ../../index.rst:94
121 | msgid ":ref:`genindex`"
122 | msgstr ":ref:`genindex`"
123 |
124 | #: ../../index.rst:95
125 | msgid ":ref:`modindex`"
126 | msgstr ":ref:`modindex`"
127 |
128 | #: ../../index.rst:96
129 | msgid ":ref:`search`"
130 | msgstr ":ref:`search`"
131 |
132 | #~ msgid "为影视行业设计的Python时码库,支持HFR高帧率以及其他丰富的功能。"
133 | #~ msgstr "为影视行业设计的 Python 时码库,支持 HFR 高帧率以及其他丰富的功能。"
134 |
135 |
--------------------------------------------------------------------------------
/docs/locale/zh_CN/LC_MESSAGES/installation.po:
--------------------------------------------------------------------------------
1 | # DFTT Timecode Documentation Chinese Translation
2 | # Copyright (C) 2025, You Ziyuan
3 | # This file is distributed under the same license as the DFTT Timecode
4 | # package.
5 | # You Ziyuan , 2025.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: DFTT Timecode 0.0.15a2\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2025-10-21 11:37+0800\n"
12 | "PO-Revision-Date: 2025-10-21 11:40+0800\n"
13 | "Last-Translator: You Ziyuan \n"
14 | "Language: zh_CN\n"
15 | "Language-Team: zh_CN \n"
16 | "Plural-Forms: nplurals=1; plural=0;\n"
17 | "MIME-Version: 1.0\n"
18 | "Content-Type: text/plain; charset=utf-8\n"
19 | "Content-Transfer-Encoding: 8bit\n"
20 | "Generated-By: Babel 2.17.0\n"
21 |
22 | #: ../../installation.rst:2
23 | msgid "Installation"
24 | msgstr "安装"
25 |
26 | #: ../../installation.rst:5
27 | msgid "Requirements"
28 | msgstr "系统要求"
29 |
30 | #: ../../installation.rst:7
31 | msgid "Python >= 3.11"
32 | msgstr "Python >= 3.11"
33 |
34 | #: ../../installation.rst:8
35 | msgid "Standard library dependencies only (no external dependencies required)"
36 | msgstr "仅依赖标准库(无需外部依赖)"
37 |
38 | #: ../../installation.rst:10
39 | msgid "fractions"
40 | msgstr "fractions"
41 |
42 | #: ../../installation.rst:11
43 | msgid "logging"
44 | msgstr "logging"
45 |
46 | #: ../../installation.rst:12
47 | msgid "math"
48 | msgstr "math"
49 |
50 | #: ../../installation.rst:13
51 | msgid "functools"
52 | msgstr "functools"
53 |
54 | #: ../../installation.rst:14
55 | msgid "re"
56 | msgstr "re"
57 |
58 | #: ../../installation.rst:17
59 | msgid "Installation from PyPI"
60 | msgstr "从 PyPI 安装"
61 |
62 | #: ../../installation.rst:19
63 | msgid "The easiest way to install dftt_timecode is using pip or uv:"
64 | msgstr "安装 dftt_timecode 最简单的方法是使用 pip 或 uv:"
65 |
66 | #: ../../installation.rst:30
67 | msgid "Installation from Source"
68 | msgstr "从源码安装"
69 |
70 | #: ../../installation.rst:32
71 | msgid ""
72 | "For development, we recommend using **uv** for faster and more reliable "
73 | "dependency management:"
74 | msgstr "对于开发,我们建议使用 **uv** 以获得更快速和可靠的依赖管理:"
75 |
76 | #: ../../installation.rst:46
77 | msgid "This will:"
78 | msgstr "这将:"
79 |
80 | #: ../../installation.rst:48
81 | msgid "Create a virtual environment in ``.venv``"
82 | msgstr "在 ``.venv`` 中创建虚拟环境"
83 |
84 | #: ../../installation.rst:49
85 | msgid "Install all development dependencies (pytest, sphinx, etc.)"
86 | msgstr "安装所有开发依赖(pytest、sphinx 等)"
87 |
88 | #: ../../installation.rst:50
89 | msgid "Install the package in editable mode"
90 | msgstr "以可编辑模式安装包"
91 |
92 | #: ../../installation.rst:52
93 | msgid "Alternatively, using pip:"
94 | msgstr "或者,使用 pip:"
95 |
96 | #: ../../installation.rst:61
97 | msgid "Verifying Installation"
98 | msgstr "验证安装"
99 |
100 | #: ../../installation.rst:63
101 | msgid "You can verify the installation by importing the package:"
102 | msgstr "您可以通过导入包来验证安装:"
103 |
104 | #: ../../installation.rst:71
105 | msgid "Development Dependencies"
106 | msgstr "开发依赖"
107 |
108 | #: ../../installation.rst:73
109 | msgid ""
110 | "The project uses **uv** for dependency management. All dependencies are "
111 | "defined in ``pyproject.toml``:"
112 | msgstr "项目使用 **uv** 进行依赖管理。所有依赖都定义在 ``pyproject.toml`` 中:"
113 |
114 | #: ../../installation.rst:75
115 | msgid "**pytest** - Testing framework"
116 | msgstr "**pytest** - 测试框架"
117 |
118 | #: ../../installation.rst:76
119 | msgid "**sphinx** - Documentation generator"
120 | msgstr "**sphinx** - 文档生成器"
121 |
122 | #: ../../installation.rst:77
123 | msgid "**pydata-sphinx-theme** - Documentation theme"
124 | msgstr "**pydata-sphinx-theme** - 文档主题"
125 |
126 | #: ../../installation.rst:79
127 | msgid "To install development dependencies with uv:"
128 | msgstr "使用 uv 安装开发依赖:"
129 |
130 | #: ../../installation.rst:89
131 | msgid "To run tests:"
132 | msgstr "运行测试:"
133 |
134 | #: ../../installation.rst:95
135 | msgid "To build documentation:"
136 | msgstr "构建文档:"
137 |
--------------------------------------------------------------------------------
/docs/api/error.rst:
--------------------------------------------------------------------------------
1 | Error Handling
2 | ==============
3 |
4 | .. currentmodule:: dftt_timecode.error
5 |
6 | Exception Classes
7 | -----------------
8 |
9 | The dftt_timecode library defines custom exception classes for different error conditions.
10 |
11 | DFTTError
12 | ~~~~~~~~~
13 |
14 | .. autoclass:: DFTTError
15 | :members:
16 | :show-inheritance:
17 |
18 | Base exception class for all dftt_timecode errors.
19 |
20 | DFTTTimecodeValueError
21 | ~~~~~~~~~~~~~~~~~~~~~~
22 |
23 | .. autoclass:: DFTTTimecodeValueError
24 | :members:
25 | :show-inheritance:
26 |
27 | Raised when an invalid timecode value is provided.
28 |
29 | DFTTTimecodeInitializationError
30 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
31 |
32 | .. autoclass:: DFTTTimecodeInitializationError
33 | :members:
34 | :show-inheritance:
35 |
36 | Raised when timecode initialization fails due to incompatible parameters.
37 |
38 | DFTTTimecodeTypeError
39 | ~~~~~~~~~~~~~~~~~~~~~
40 |
41 | .. autoclass:: DFTTTimecodeTypeError
42 | :members:
43 | :show-inheritance:
44 |
45 | Raised when an invalid timecode type is specified or type mismatch occurs.
46 |
47 | DFTTTimecodeOperatorError
48 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
49 |
50 | .. autoclass:: DFTTTimecodeOperatorError
51 | :members:
52 | :show-inheritance:
53 |
54 | Raised when an arithmetic or comparison operation on timecode objects fails.
55 |
56 | DFTTTimeRangeMethodError
57 | ~~~~~~~~~~~~~~~~~~~~~~~~~
58 |
59 | .. autoclass:: DFTTTimeRangeMethodError
60 | :members:
61 | :show-inheritance:
62 |
63 | Raised when a timerange method is called with invalid parameters or conditions.
64 |
65 | DFTTTimeRangeValueError
66 | ~~~~~~~~~~~~~~~~~~~~~~~~
67 |
68 | .. autoclass:: DFTTTimeRangeValueError
69 | :members:
70 | :show-inheritance:
71 |
72 | Raised when a timerange value is invalid or out of acceptable range.
73 |
74 | DFTTTimeRangeTypeError
75 | ~~~~~~~~~~~~~~~~~~~~~~
76 |
77 | .. autoclass:: DFTTTimeRangeTypeError
78 | :members:
79 | :show-inheritance:
80 |
81 | Raised when a timerange type or operand type is invalid.
82 |
83 | DFTTTimeRangeFPSError
84 | ~~~~~~~~~~~~~~~~~~~~~
85 |
86 | .. autoclass:: DFTTTimeRangeFPSError
87 | :members:
88 | :show-inheritance:
89 |
90 | Raised when timerange operations fail due to frame rate mismatches.
91 |
92 | Error Examples
93 | --------------
94 |
95 | Invalid Timecode Value
96 | ~~~~~~~~~~~~~~~~~~~~~~
97 |
98 | .. code-block:: python
99 |
100 | from dftt_timecode import DfttTimecode
101 | from dftt_timecode.error import DFTTTimecodeValueError
102 |
103 | try:
104 | tc = DfttTimecode('99:99:99:99', 'smpte', fps=24)
105 | except DFTTTimecodeValueError as e:
106 | print(f"Invalid timecode: {e}")
107 |
108 | Timecode Initialization Error
109 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
110 |
111 | .. code-block:: python
112 |
113 | from dftt_timecode import DfttTimecode
114 | from dftt_timecode.error import DFTTTimecodeInitializationError
115 |
116 | try:
117 | # Drop-frame status mismatch with timecode format
118 | tc = DfttTimecode('01:00:00:00', 'smpte', fps=29.97, drop_frame=False)
119 | except DFTTTimecodeInitializationError as e:
120 | print(f"Initialization error: {e}")
121 |
122 | Type Error
123 | ~~~~~~~~~~
124 |
125 | .. code-block:: python
126 |
127 | from dftt_timecode import DfttTimecode
128 | from dftt_timecode.error import DFTTTimecodeTypeError
129 |
130 | try:
131 | tc = DfttTimecode('invalid_format', 'unknown_type', fps=24)
132 | except DFTTTimecodeTypeError as e:
133 | print(f"Type error: {e}")
134 |
135 | Operator Error
136 | ~~~~~~~~~~~~~~
137 |
138 | .. code-block:: python
139 |
140 | from dftt_timecode import DfttTimecode
141 | from dftt_timecode.error import DFTTTimecodeOperatorError
142 |
143 | try:
144 | tc1 = DfttTimecode('01:00:00:00', 'auto', fps=24)
145 | tc2 = DfttTimecode('01:00:00:00', 'auto', fps=30)
146 | # Cannot add timecodes with different frame rates
147 | result = tc1 + tc2
148 | except DFTTTimecodeOperatorError as e:
149 | print(f"Operator error: {e}")
150 |
--------------------------------------------------------------------------------
/docs/locale/zh_CN/LC_MESSAGES/README.po:
--------------------------------------------------------------------------------
1 | # DFTT Timecode Documentation Chinese Translation
2 | # Copyright (C) 2025, You Ziyuan
3 | # This file is distributed under the same license as the DFTT Timecode
4 | # package.
5 | # You Ziyuan , 2025.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: DFTT Timecode 0.0.15a2\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2025-10-21 11:37+0800\n"
12 | "PO-Revision-Date: 2025-10-21 12:15+0800\n"
13 | "Last-Translator: You Ziyuan \n"
14 | "Language: zh_CN\n"
15 | "Language-Team: zh_CN \n"
16 | "Plural-Forms: nplurals=1; plural=0;\n"
17 | "MIME-Version: 1.0\n"
18 | "Content-Type: text/plain; charset=utf-8\n"
19 | "Content-Transfer-Encoding: 8bit\n"
20 | "Generated-By: Babel 2.17.0\n"
21 |
22 | #: ../../README.md:1
23 | msgid "Documentation"
24 | msgstr "文档"
25 |
26 | #: ../../README.md:3
27 | msgid "This directory contains the Sphinx documentation for dftt_timecode."
28 | msgstr "此目录包含 dftt_timecode 的 Sphinx 文档。"
29 |
30 | #: ../../README.md:5
31 | msgid "Building Documentation Locally"
32 | msgstr "本地构建文档"
33 |
34 | #: ../../README.md:7
35 | msgid "Prerequisites"
36 | msgstr "前提条件"
37 |
38 | #: ../../README.md:9
39 | msgid "Install the required dependencies:"
40 | msgstr "安装所需的依赖:"
41 |
42 | #: ../../README.md:15
43 | msgid "Build HTML Documentation"
44 | msgstr "构建 HTML 文档"
45 |
46 | #: ../../README.md:17
47 | msgid "On Unix/macOS:"
48 | msgstr "在 Unix/macOS 上:"
49 |
50 | #: ../../README.md:24
51 | msgid "On Windows:"
52 | msgstr "在 Windows 上:"
53 |
54 | #: ../../README.md:31
55 | msgid "The built documentation will be available in `docs/_build/html/`."
56 | msgstr "构建的文档将在 `docs/_build/html/` 中提供。"
57 |
58 | #: ../../README.md:33
59 | msgid "View Documentation"
60 | msgstr "查看文档"
61 |
62 | #: ../../README.md:35
63 | msgid "Open `docs/_build/html/index.html` in your web browser."
64 | msgstr "在 Web 浏览器中打开 `docs/_build/html/index.html`。"
65 |
66 | #: ../../README.md:37
67 | msgid "Other Build Formats"
68 | msgstr "其他构建格式"
69 |
70 | #: ../../README.md:45
71 | msgid "Automatic Deployment"
72 | msgstr "自动部署"
73 |
74 | #: ../../README.md:47
75 | msgid ""
76 | "Documentation is automatically built and deployed to GitHub Pages when "
77 | "changes are pushed to the main branch."
78 | msgstr "当更改推送到主分支时,文档会自动构建并部署到 GitHub Pages。"
79 |
80 | #: ../../README.md:49
81 | msgid ""
82 | "The deployment is handled by the GitHub Actions workflow in "
83 | "`.github/workflows/docs.yml`."
84 | msgstr "部署由 `.github/workflows/docs.yml` 中的 GitHub Actions 工作流处理。"
85 |
86 | #: ../../README.md:51
87 | msgid "Documentation Structure"
88 | msgstr "文档结构"
89 |
90 | #: ../../README.md:53
91 | msgid "`index.rst` - Home page"
92 | msgstr "`index.rst` - 主页"
93 |
94 | #: ../../README.md:54
95 | msgid "`installation.rst` - Installation instructions"
96 | msgstr "`installation.rst` - 安装说明"
97 |
98 | #: ../../README.md:55
99 | msgid "`quickstart.rst` - Quick start guide"
100 | msgstr "`quickstart.rst` - 快速入门指南"
101 |
102 | #: ../../README.md:56
103 | msgid "`user_guide.rst` - Comprehensive user guide"
104 | msgstr "`user_guide.rst` - 综合用户指南"
105 |
106 | #: ../../README.md:57
107 | msgid "`api/` - API reference documentation"
108 | msgstr "`api/` - API 参考文档"
109 |
110 | #: ../../README.md:58
111 | msgid "`contributing.rst` - Contributing guidelines"
112 | msgstr "`contributing.rst` - 贡献指南"
113 |
114 | #: ../../README.md:59
115 | msgid "`changelog.rst` - Version history"
116 | msgstr "`changelog.rst` - 版本历史"
117 |
118 | #: ../../README.md:60
119 | msgid "`conf.py` - Sphinx configuration"
120 | msgstr "`conf.py` - Sphinx 配置"
121 |
122 | #: ../../README.md:61
123 | msgid "`_static/` - Static files (CSS, images, etc.)"
124 | msgstr "`_static/` - 静态文件(CSS、图片等)"
125 |
126 | #: ../../README.md:62
127 | msgid "`_templates/` - Custom templates"
128 | msgstr "`_templates/` - 自定义模板"
129 |
130 | #: ../../README.md:64
131 | msgid "Writing Documentation"
132 | msgstr "编写文档"
133 |
134 | #: ../../README.md:66
135 | msgid "Documentation is written in reStructuredText (RST) format"
136 | msgstr "文档以 reStructuredText(RST)格式编写"
137 |
138 | #: ../../README.md:67
139 | msgid "API documentation is auto-generated from docstrings using Sphinx autodoc"
140 | msgstr "API 文档使用 Sphinx autodoc 从文档字符串自动生成"
141 |
142 | #: ../../README.md:68
143 | msgid "Follow the existing style and structure when adding new pages"
144 | msgstr "添加新页面时遵循现有的风格和结构"
145 |
146 | #: ../../README.md:69
147 | msgid "Build and preview locally before committing changes"
148 | msgstr "在提交更改之前在本地构建和预览"
149 |
--------------------------------------------------------------------------------
/dftt_timecode/pattern.py:
--------------------------------------------------------------------------------
1 | """
2 | Regular expression patterns for timecode format validation.
3 |
4 | This module defines regex patterns for matching various professional timecode formats
5 | used in film, television, and video production. All patterns support negative timecodes
6 | with an optional leading minus sign.
7 | """
8 |
9 | import re
10 |
11 | SMPTE_NDF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d):){1}(\d?\d\d{1}){1}$')
12 | """Regex pattern for SMPTE Non-Drop-Frame (NDF) timecode format.
13 |
14 | Matches: ``HH:MM:SS:FF`` where frames use colon separator.
15 |
16 | Examples:
17 | - Standard: ``01:23:45:12`` or ``-01:23:45:12``
18 | - High frame rate (>=100fps): ``01:01:23:45:102`` or ``-01:01:23:45:102``
19 |
20 | Note:
21 | The colon separator before frames distinguishes NDF from drop-frame format.
22 | """
23 |
24 | SMPTE_DF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d);){1}(\d?\d\d{1}){1}$')
25 | """Regex pattern for SMPTE Drop-Frame (DF) timecode format.
26 |
27 | Matches: ``HH:MM:SS;FF`` where frames use semicolon separator.
28 |
29 | Examples:
30 | - Standard: ``01:23:45;12`` or ``-01:23:45;12``
31 | - High frame rate (>=100fps): ``01:00:00;102`` or ``-01:00:00;102``
32 |
33 | Note:
34 | The semicolon separator before frames indicates drop-frame format.
35 | Used primarily with 29.97 fps and its multiples (59.94, 119.88).
36 | """
37 |
38 | SMPTE_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d);?:?){1}(\d?\d\d{1}){1}$')
39 | """Regex pattern for any SMPTE timecode format (both NDF and DF).
40 |
41 | Matches: ``HH:MM:SS:FF`` or ``HH:MM:SS;FF``
42 |
43 | This is the union of :data:`SMPTE_NDF_REGEX` and :data:`SMPTE_DF_REGEX` patterns,
44 | accepting either colon or semicolon before the frame number.
45 | """
46 |
47 | SRT_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d),){1}(\d\d\d){1}$')
48 | """Regex pattern for SubRip (SRT) subtitle timecode format.
49 |
50 | Matches: ``HH:MM:SS,mmm`` where mmm is milliseconds.
51 |
52 | Examples:
53 | - Standard: ``01:23:45,678`` or ``-01:23:45,678``
54 |
55 | Note:
56 | Uses comma separator before milliseconds. This format is frame rate independent.
57 | """
58 |
59 | FFMPEG_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d)\.){1}(\d?\d+){1}$')
60 | """Regex pattern for FFmpeg timecode format.
61 |
62 | Matches: ``HH:MM:SS.ss`` where ss is sub-seconds (centiseconds).
63 |
64 | Examples:
65 | - Standard: ``01:23:45.67`` or ``-01:23:45.67``
66 |
67 | Note:
68 | Uses period separator before sub-seconds. Commonly used in video processing tools.
69 | """
70 |
71 | 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}$')
72 | """Regex pattern for DLP Cinema timecode format.
73 |
74 | Matches: ``HH:MM:SS:sss`` where sss is sub-frames (0-249).
75 |
76 | Examples:
77 | - Standard: ``01:23:45:102`` or ``-01:23:45:102``
78 |
79 | Note:
80 | DLP uses 250 sub-frames per second (4ms per sub-frame).
81 | See `CineCanvas specification `_ page 17.
82 | """
83 |
84 | FCPX_REGEX = re.compile(r'^(?:-)?(\d+)[/](\d+)?s$')
85 | """Regex pattern for Final Cut Pro X (FCPX) rational time format.
86 |
87 | Matches: ``numerator/denominator`` or ``numerator`` followed by ``s``.
88 |
89 | Examples:
90 | - Fraction: ``1/24s`` or ``-1/24s``
91 | - Integer: ``1234s`` or ``-1234s``
92 |
93 | Note:
94 | Represents time as a rational number (fraction) for precise calculations.
95 | The denominator is optional for integer values.
96 | """
97 |
98 | FRAME_REGEX = re.compile(r'^(-?\d+?)f?$')
99 | """Regex pattern for frame count format.
100 |
101 | Matches: Frame number with optional ``f`` suffix.
102 |
103 | Examples:
104 | - With suffix: ``1234f`` or ``-1234f``
105 | - Without suffix: ``1234`` or ``-1234``
106 |
107 | Note:
108 | Represents timecode as an absolute frame count. Requires frame rate for conversion.
109 | """
110 |
111 | TIME_REGEX = re.compile(r'^(-?\d+?(\.{1})\d+?|-?\d+?)s?$')
112 | """Regex pattern for timestamp format in seconds.
113 |
114 | Matches: Decimal or integer seconds with optional ``s`` suffix.
115 |
116 | Examples:
117 | - Decimal: ``1234.5s`` or ``-1234.5``
118 | - Integer: ``1234s`` or ``-1234``
119 |
120 | Note:
121 | Represents timecode as absolute seconds from zero. Most precise internal format.
122 | """
123 |
124 |
125 |
--------------------------------------------------------------------------------
/docs/user_guide.rst:
--------------------------------------------------------------------------------
1 | User Guide
2 | ==========
3 |
4 | This comprehensive guide covers all aspects of using dftt_timecode.
5 |
6 | Overview
7 | --------
8 |
9 | dftt_timecode is a comprehensive Python library designed for the film and TV industry to handle timecodes
10 | in various formats with high precision. It supports high frame rates (HFR) up to 999.99 fps and provides
11 | a rich set of operations for timecode manipulation.
12 |
13 | Key Concepts
14 | ------------
15 |
16 | Timecode Types
17 | ~~~~~~~~~~~~~~
18 |
19 | The library supports multiple timecode formats used in different contexts:
20 |
21 | - **SMPTE**: Industry-standard format (HH:MM:SS:FF)
22 | - **SRT**: SubRip subtitle format (HH:MM:SS,mmm)
23 | - **FFMPEG**: FFmpeg timestamp format (HH:MM:SS.ff)
24 | - **FCPX**: Final Cut Pro X format (frames/fps)
25 | - **DLP**: Digital cinema format (HH:MM:SS:FFF)
26 | - **Frame**: Simple frame count
27 | - **Time**: Seconds-based timestamp
28 |
29 | Frame Rates
30 | ~~~~~~~~~~~
31 |
32 | The library supports:
33 |
34 | - Standard frame rates (23.976, 24, 25, 29.97, 30, 50, 59.94, 60, etc.)
35 | - High frame rates (96, 100, 120, 144, 240, etc.)
36 | - Custom frame rates from 0.01 to 999.99 fps
37 | - Precise fractional frame rates using Python's Fraction type
38 |
39 | Drop-Frame vs Non-Drop-Frame
40 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
41 |
42 | For NTSC video standards (29.97 fps, 59.94 fps), the library correctly handles:
43 |
44 | - Non-drop-frame (NDF): Uses colon separator (HH:MM:SS:FF)
45 | - Drop-frame (DF): Uses semicolon separator (HH:MM:SS;FF)
46 |
47 | Drop-frame timecode compensates for the slight discrepancy between nominal and actual frame rates
48 | by periodically skipping frame numbers.
49 |
50 | Strict Mode
51 | ~~~~~~~~~~~
52 |
53 | Strict mode ensures timecodes remain within a 24-hour cycle:
54 |
55 | - Enabled: Timecodes wrap around at 24 hours (25:00:00:00 becomes 01:00:00:00)
56 | - Disabled: Timecodes can exceed 24 hours (useful for long-form content)
57 |
58 | Common Use Cases
59 | ----------------
60 |
61 | Video Editing
62 | ~~~~~~~~~~~~~
63 |
64 | .. code-block:: python
65 |
66 | from dftt_timecode import DfttTimecode
67 |
68 | # Define edit points
69 | in_point = DfttTimecode('01:05:23:12', 'auto', fps=23.976)
70 | out_point = DfttTimecode('01:08:45:18', 'auto', fps=23.976)
71 |
72 | # Calculate duration
73 | duration = out_point - in_point
74 | print(f"Clip duration: {duration.timecode_output('smpte')}")
75 |
76 | Subtitle Timing
77 | ~~~~~~~~~~~~~~~
78 |
79 | .. code-block:: python
80 |
81 | # SRT format for subtitles
82 | start = DfttTimecode('00:01:23,456', 'auto', fps=25)
83 | end = DfttTimecode('00:01:27,890', 'auto', fps=25)
84 |
85 | # Convert to SMPTE for editing
86 | print(f"Start: {start.timecode_output('smpte')}")
87 | print(f"End: {end.timecode_output('smpte')}")
88 |
89 | Frame Rate Conversion
90 | ~~~~~~~~~~~~~~~~~~~~~
91 |
92 | .. code-block:: python
93 |
94 | # Convert from 24fps to 30fps
95 | tc_24 = DfttTimecode('01:00:00:00', 'auto', fps=24)
96 | tc_24.set_fps(30, rounding=True)
97 | print(tc_24.timecode_output('smpte'))
98 |
99 | High Frame Rate Workflows
100 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
101 |
102 | .. code-block:: python
103 |
104 | # Working with high frame rate content
105 | hfr_tc = DfttTimecode('00:10:00:000', 'auto', fps=120)
106 |
107 | # Convert to standard frame rate for delivery
108 | hfr_tc.set_fps(24, rounding=True)
109 | print(hfr_tc.timecode_output('smpte'))
110 |
111 | Best Practices
112 | --------------
113 |
114 | 1. **Use Strict Mode for Standard Workflows**: Enable strict mode for typical video editing to prevent
115 | timecode values from exceeding 24 hours.
116 |
117 | 2. **Specify Frame Rates Explicitly**: Always specify the correct frame rate when creating timecode
118 | objects to ensure accurate conversions.
119 |
120 | 3. **Use Fraction for Precise Frame Rates**: For frame rates like 23.976 or 29.97, use Fraction
121 | for maximum precision:
122 |
123 | .. code-block:: python
124 |
125 | from fractions import Fraction
126 | fps = Fraction(24000, 1001) # Exactly 23.976023976...
127 |
128 | 4. **Handle Drop-Frame Correctly**: When working with NTSC frame rates (29.97, 59.94), ensure
129 | drop-frame is set correctly based on your workflow requirements.
130 |
131 | 5. **Validate User Input**: Use try-except blocks to catch and handle timecode errors gracefully:
132 |
133 | .. code-block:: python
134 |
135 | from dftt_timecode.error import DfttTimecodeError
136 |
137 | try:
138 | tc = DfttTimecode(user_input, 'auto', fps=24)
139 | except DfttTimecodeError as e:
140 | print(f"Invalid timecode: {e}")
141 |
142 | Performance Considerations
143 | --------------------------
144 |
145 | The library uses high-precision Fraction internally for timestamp storage, which ensures
146 | accuracy but may be slower than floating-point arithmetic. For performance-critical applications:
147 |
148 | - Create timecode objects once and reuse them
149 | - Use frame count operations when possible (integer arithmetic is faster)
150 | - Consider caching converted values if the same conversions are needed repeatedly
151 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | import os
7 | import sys
8 | import tomllib
9 |
10 | # -- Path setup --------------------------------------------------------------
11 | # If extensions (or modules to document with autodoc) are in another directory,
12 | # add these directories to sys.path here.
13 | sys.path.insert(0, os.path.abspath('..'))
14 |
15 | # -- Read project metadata from pyproject.toml -------------------------------
16 | # Get the path to pyproject.toml (one level up from docs/)
17 | docs_dir = os.path.dirname(os.path.abspath(__file__))
18 | project_root = os.path.dirname(docs_dir)
19 | pyproject_path = os.path.join(project_root, 'pyproject.toml')
20 |
21 | with open(pyproject_path, 'rb') as f:
22 | pyproject_data = tomllib.load(f)
23 |
24 | project_info = pyproject_data['project']
25 |
26 | # -- Project information -----------------------------------------------------
27 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
28 |
29 | project = 'DFTT Timecode'
30 | copyright = '2025, You Ziyuan'
31 | author = ', '.join(a['name'] for a in project_info['authors'])
32 | release = project_info['version']
33 | version = release # Short version
34 |
35 | # HTML title
36 | html_title = 'DFTT Timecode'
37 |
38 | # -- General configuration ---------------------------------------------------
39 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
40 |
41 | # Master document
42 | master_doc = 'index'
43 | root_doc = 'index'
44 |
45 | extensions = [
46 | 'sphinx.ext.autodoc',
47 | 'sphinx.ext.napoleon',
48 | 'sphinx.ext.viewcode',
49 | 'sphinx.ext.intersphinx',
50 | 'sphinx.ext.autosummary',
51 | 'myst_parser',
52 | ]
53 |
54 | templates_path = ['_templates']
55 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
56 |
57 | # -- Internationalization (i18n) configuration -------------------------------
58 | # https://www.sphinx-doc.org/en/master/usage/advanced/intl.html
59 |
60 | # Default language (English)
61 | language = 'en'
62 |
63 | # Directory path for message catalogs
64 | locale_dirs = ['locale/']
65 |
66 | # Generate .pot files with message catalogs for each document
67 | gettext_compact = False
68 |
69 | # Additional languages for documentation
70 | # This is used by the language switcher
71 | gettext_additional_targets = ['index']
72 |
73 | # -- Options for HTML output -------------------------------------------------
74 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
75 |
76 | html_theme = 'pydata_sphinx_theme'
77 | html_static_path = ['_static']
78 |
79 | # PyData theme specific options
80 | html_theme_options = {
81 | "github_url": "https://github.com/OwenYou/dftt_timecode",
82 | "use_edit_page_button": True,
83 | "show_toc_level": 2,
84 | "navbar_align": "left",
85 | "navigation_with_keys": False,
86 | "navigation_depth": 4,
87 | "collapse_navigation": False, # Keep navigation expanded
88 | "logo": {
89 | "text": "DFTT Timecode",
90 | },
91 | "navbar_end": ["navbar-icon-links", "language-switcher"],
92 | "primary_sidebar_end": [], # Remove primary sidebar content
93 | "show_nav_level": 0, # Hide navigation levels
94 | "switcher": {
95 | "json_url": "https://owenyou.github.io/dftt_timecode/_static/switcher.json",
96 | "version_match": os.environ.get("READTHEDOCS_LANGUAGE", language),
97 | },
98 | }
99 |
100 | # Disable the left sidebar navigation
101 | html_sidebars = {
102 | "**": []
103 | }
104 |
105 | html_context = {
106 | "github_user": "OwenYou",
107 | "github_repo": "dftt_timecode",
108 | "github_version": "main",
109 | "doc_path": "docs",
110 | }
111 |
112 | # -- Options for autodoc ----------------------------------------------------
113 | autodoc_default_options = {
114 | 'members': True,
115 | 'member-order': 'bysource',
116 | 'special-members': '__init__',
117 | 'undoc-members': True,
118 | 'exclude-members': '__weakref__'
119 | }
120 |
121 | # Avoid documenting imports as duplicates
122 | autodoc_typehints = 'description'
123 | autodoc_class_signature = 'separated'
124 |
125 | # Control which module path to use for imported members
126 | # This prevents duplicate documentation when a class is imported into __init__.py
127 | add_module_names = False
128 |
129 | # Napoleon settings for Google/NumPy style docstrings
130 | napoleon_google_docstring = True
131 | napoleon_numpy_docstring = True
132 | napoleon_include_init_with_doc = True
133 | napoleon_include_private_with_doc = False
134 | napoleon_include_special_with_doc = True
135 | napoleon_use_admonition_for_examples = False
136 | napoleon_use_admonition_for_notes = False
137 | napoleon_use_admonition_for_references = False
138 | napoleon_use_ivar = False
139 | napoleon_use_param = True
140 | napoleon_use_rtype = True
141 | napoleon_preprocess_types = False
142 | napoleon_type_aliases = None
143 | napoleon_attr_annotations = True
144 |
145 | # Intersphinx mapping
146 | intersphinx_mapping = {
147 | 'python': ('https://docs.python.org/3', None),
148 | }
149 |
150 | # -- Options for autosummary ------------------------------------------------
151 | # Disable autosummary generation to avoid duplicate documentation
152 | autosummary_generate = False
153 |
154 | # -- Options for MyST-Parser ------------------------------------------------
155 | # Enable markdown files to be parsed by Sphinx
156 | myst_enable_extensions = [
157 | "colon_fence", # ::: syntax for directives
158 | "deflist", # Definition lists
159 | "html_image", # HTML image syntax
160 | ]
161 | myst_heading_anchors = 3 # Auto-generate anchors for headings up to level 3
162 |
--------------------------------------------------------------------------------
/dftt_timecode/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | DFTT Timecode Library for Film and Television Production.
3 |
4 | A high-precision timecode library supporting multiple professional formats including
5 | SMPTE (drop-frame and non-drop-frame), SRT, FFmpeg, FCPX, DLP Cinema, and more.
6 | Supports high frame rates from 0.01 to 999.99 fps with frame-accurate calculations.
7 |
8 | Main Classes:
9 | - :class:`DfttTimecode`: Core timecode class with format conversion and arithmetic operations
10 | - :class:`DfttTimeRange`: Timerange class for working with time intervals
11 |
12 | Convenience Aliases:
13 | - :func:`timecode`: Alias for DfttTimecode
14 | - :func:`dtc`: Short alias for DfttTimecode
15 | - :func:`timerange`: Alias for DfttTimeRange
16 | - :func:`dtr`: Short alias for DfttTimeRange
17 |
18 | Example:
19 | >>> from dftt_timecode import DfttTimecode
20 | >>> tc = DfttTimecode('01:00:00:00', fps=24)
21 | >>> print(tc.timecode_output('srt'))
22 | 01:00:00,000
23 | >>> tc2 = tc + 100 # Add 100 frames
24 | >>> print(tc2)
25 | 01:00:04:04
26 | """
27 |
28 | from fractions import Fraction
29 | from typing import Optional
30 | from dftt_timecode.core.dftt_timecode import DfttTimecode, TimecodeType
31 | from dftt_timecode.core.dftt_timerange import DfttTimeRange
32 | from dftt_timecode.logging_config import configure_logging, get_logger
33 |
34 | # Read version from package metadata (populated from pyproject.toml)
35 | try:
36 | from importlib.metadata import version, PackageNotFoundError
37 |
38 | try:
39 | __version__ = version("dftt-timecode")
40 | except PackageNotFoundError:
41 | # Package is not installed, use a fallback version
42 | __version__ = "0.0.0+dev"
43 | except ImportError:
44 | # Python < 3.8 fallback (though we require 3.11+)
45 | __version__ = "0.0.0+dev"
46 |
47 |
48 | # Aliases for easier importing
49 | def Timecode(
50 | timecode_value,
51 | timecode_type: TimecodeType = "auto",
52 | fps=24.0,
53 | drop_frame=None,
54 | strict=True,
55 | ) -> DfttTimecode:
56 | """Create a DfttTimecode instance.
57 |
58 | This is an alias for :class:`DfttTimecode` constructor.
59 |
60 | Args:
61 | *args: Positional arguments passed to DfttTimecode
62 | **kwargs: Keyword arguments passed to DfttTimecode
63 |
64 | Returns:
65 | DfttTimecode: A new timecode instance
66 |
67 | Example:
68 | >>> tc = Timecode('01:00:00:00', fps=24)
69 | """
70 | return DfttTimecode(
71 | timecode_value,
72 | timecode_type=timecode_type,
73 | fps=fps,
74 | drop_frame=drop_frame,
75 | strict=strict,
76 | )
77 |
78 |
79 | def Timerange(
80 | start_tc=None,
81 | end_tc=None,
82 | forward: bool = True,
83 | fps=24.0,
84 | start_precise_time: Optional[Fraction] = None,
85 | precise_duration: Optional[Fraction] = None,
86 | strict_24h: bool = False,
87 | ) -> DfttTimeRange:
88 | """Create a DfttTimeRange instance.
89 |
90 | This is an alias for :class:`DfttTimeRange` constructor.
91 |
92 | Args:
93 | *args: Positional arguments passed to DfttTimeRange
94 | **kwargs: Keyword arguments passed to DfttTimeRange
95 |
96 | Returns:
97 | DfttTimeRange: A new timerange instance
98 |
99 | Example:
100 | >>> tr = Timerange('01:00:00:00', '02:00:00:00', fps=24)
101 | """
102 | return DfttTimeRange(
103 | start_tc=start_tc,
104 | end_tc=end_tc,
105 | fps=fps,
106 | forward=forward,
107 | start_precise_time=start_precise_time,
108 | precise_duration=precise_duration,
109 | strict_24h=strict_24h,
110 | )
111 |
112 |
113 | def dtc(
114 | timecode_value,
115 | timecode_type: TimecodeType = "auto",
116 | fps=24.0,
117 | drop_frame=None,
118 | strict=True,
119 | ) -> DfttTimecode:
120 | """Create a DfttTimecode instance (short alias).
121 |
122 | This is a short alias for :class:`DfttTimecode` constructor.
123 |
124 | Args:
125 | *args: Positional arguments passed to DfttTimecode
126 | **kwargs: Keyword arguments passed to DfttTimecode
127 |
128 | Returns:
129 | DfttTimecode: A new timecode instance
130 |
131 | Example:
132 | >>> tc = dtc('01:00:00:00', fps=24)
133 | """
134 | return DfttTimecode(
135 | timecode_value,
136 | timecode_type=timecode_type,
137 | fps=fps,
138 | drop_frame=drop_frame,
139 | strict=strict,
140 | )
141 |
142 |
143 | def dtr(
144 | start_tc=None,
145 | end_tc=None,
146 | forward: bool = True,
147 | fps=24.0,
148 | start_precise_time: Optional[Fraction] = None,
149 | precise_duration: Optional[Fraction] = None,
150 | strict_24h: bool = False,
151 | ) -> DfttTimeRange:
152 | """Create a DfttTimeRange instance (short alias).
153 |
154 | This is a short alias for :class:`DfttTimeRange` constructor.
155 |
156 | Args:
157 | *args: Positional arguments passed to DfttTimeRange
158 | **kwargs: Keyword arguments passed to DfttTimeRange
159 |
160 | Returns:
161 | DfttTimeRange: A new timerange instance
162 |
163 | Example:
164 | >>> tr = dtr('01:00:00:00', '02:00:00:00', fps=24)
165 | """
166 | return DfttTimeRange(
167 | start_tc=start_tc,
168 | end_tc=end_tc,
169 | fps=fps,
170 | forward=forward,
171 | start_precise_time=start_precise_time,
172 | precise_duration=precise_duration,
173 | strict_24h=strict_24h,
174 | )
175 |
176 |
177 | name = "dftt_timecode"
178 | __author__ = "You Ziyuan"
179 |
180 | __all__ = [
181 | "DfttTimecode",
182 | "DfttTimeRange",
183 | "timecode",
184 | "timerange",
185 | "dtc",
186 | "dtr",
187 | "configure_logging",
188 | "get_logger",
189 | ]
190 |
--------------------------------------------------------------------------------
/dftt_timecode/logging_config.py:
--------------------------------------------------------------------------------
1 | """
2 | Logging configuration for DFTT Timecode library.
3 |
4 | Automatically adjusts log level based on development context:
5 | - **Installed packages** (from PyPI/build): INFO level (production default)
6 | - **Main branch**: INFO level (production-ready code after PR)
7 | - **Development branches** (dev, feature/*): DEBUG level (detailed tracing)
8 |
9 | The logging level can be overridden with the DFTT_LOG_LEVEL environment variable.
10 |
11 | Example:
12 | # Use default log level (INFO for installed packages, branch-dependent for development)
13 | from dftt_timecode import DfttTimecode
14 | tc = DfttTimecode('01:00:00:00', fps=24)
15 |
16 | # Override log level via environment variable
17 | import os
18 | os.environ['DFTT_LOG_LEVEL'] = 'WARNING'
19 |
20 | # Or configure programmatically
21 | import logging
22 | from dftt_timecode import configure_logging
23 | configure_logging(logging.DEBUG)
24 | """
25 |
26 | import logging
27 | import os
28 | import subprocess
29 | from typing import Optional
30 |
31 |
32 | def _get_git_branch() -> Optional[str]:
33 | """
34 | Get the current git branch name.
35 |
36 | Returns:
37 | str: The current branch name, or None if not in a git repo or error occurs.
38 | """
39 | try:
40 | result = subprocess.run(
41 | ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
42 | capture_output=True,
43 | text=True,
44 | timeout=2,
45 | check=False
46 | )
47 | if result.returncode == 0:
48 | return result.stdout.strip()
49 | except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
50 | pass
51 | return None
52 |
53 |
54 | def _determine_log_level() -> int:
55 | """
56 | Determine the appropriate log level based on environment and git branch.
57 |
58 | Priority:
59 | 1. DFTT_LOG_LEVEL environment variable (if set)
60 | 2. DEBUG for development branches (dev, feature/*, etc.)
61 | 3. INFO for main branch OR when git info unavailable (built packages)
62 |
63 | The default for built/installed packages is INFO to avoid verbose output
64 | in production environments.
65 |
66 | Returns:
67 | int: logging level constant (logging.DEBUG, logging.INFO, etc.)
68 | """
69 | # Check environment variable first (highest priority)
70 | env_level = os.environ.get('DFTT_LOG_LEVEL', '').upper()
71 | if env_level:
72 | level_mapping = {
73 | 'DEBUG': logging.DEBUG,
74 | 'INFO': logging.INFO,
75 | 'WARNING': logging.WARNING,
76 | 'ERROR': logging.ERROR,
77 | 'CRITICAL': logging.CRITICAL,
78 | }
79 | if env_level in level_mapping:
80 | return level_mapping[env_level]
81 |
82 | # Check git branch
83 | branch = _get_git_branch()
84 |
85 | if branch is None:
86 | # No git info available (e.g., installed package from PyPI)
87 | # Default to INFO for production use
88 | return logging.INFO
89 | elif branch == 'main':
90 | # Main branch: production-ready code after PR
91 | return logging.INFO
92 | else:
93 | # Development/feature branches: verbose debugging
94 | return logging.DEBUG
95 |
96 |
97 | def get_logger(name: str) -> logging.Logger:
98 | """
99 | Get a configured logger for the specified module.
100 |
101 | The logger is configured with:
102 | - Appropriate log level based on git branch (INFO for main, DEBUG for others)
103 | - Formatted output with timestamp, level, file, line number, function name, and message
104 | - Stream handler for console output
105 |
106 | Args:
107 | name: Logger name (typically __name__ from the calling module)
108 |
109 | Returns:
110 | logging.Logger: Configured logger instance
111 |
112 | Example:
113 | >>> from dftt_timecode.logging_config import get_logger
114 | >>> logger = get_logger(__name__)
115 | >>> logger.debug("This is a debug message")
116 | >>> logger.info("This is an info message")
117 | """
118 | logger = logging.getLogger(name)
119 |
120 | # Avoid adding handlers multiple times
121 | if logger.handlers:
122 | return logger
123 |
124 | # Determine log level
125 | log_level = _determine_log_level()
126 | logger.setLevel(log_level)
127 |
128 | # Create formatter
129 | formatter = logging.Formatter(
130 | '%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d-%(funcName)s()] %(message)s'
131 | )
132 |
133 | # Create and configure stream handler
134 | stream_handler = logging.StreamHandler()
135 | stream_handler.setLevel(log_level)
136 | stream_handler.setFormatter(formatter)
137 |
138 | # Add handler to logger
139 | logger.addHandler(stream_handler)
140 |
141 | # Prevent propagation to root logger to avoid duplicate messages
142 | logger.propagate = False
143 |
144 | return logger
145 |
146 |
147 | def configure_logging(level: Optional[int] = None) -> None:
148 | """
149 | Configure logging for the entire dftt_timecode package.
150 |
151 | This function can be called by users to manually set the log level
152 | for all dftt_timecode loggers.
153 |
154 | Args:
155 | level: Optional logging level (logging.DEBUG, logging.INFO, etc.).
156 | If None, uses automatic detection based on git branch.
157 |
158 | Example:
159 | >>> import logging
160 | >>> from dftt_timecode.logging_config import configure_logging
161 | >>> configure_logging(logging.WARNING) # Only show warnings and above
162 | """
163 | if level is None:
164 | level = _determine_log_level()
165 |
166 | # Configure all dftt_timecode loggers
167 | for logger_name in logging.Logger.manager.loggerDict:
168 | if logger_name.startswith('dftt_timecode'):
169 | logger = logging.getLogger(logger_name)
170 | logger.setLevel(level)
171 | for handler in logger.handlers:
172 | handler.setLevel(level)
173 |
--------------------------------------------------------------------------------
/docs/quickstart.rst:
--------------------------------------------------------------------------------
1 | Quick Start
2 | ===========
3 |
4 | This guide will help you get started with dftt_timecode quickly.
5 |
6 | Importing the Library
7 | ---------------------
8 |
9 | .. code-block:: python
10 |
11 | from dftt_timecode import DfttTimecode
12 |
13 | Creating Timecode Objects
14 | --------------------------
15 |
16 | There are multiple ways to create timecode objects:
17 |
18 | SMPTE Format
19 | ~~~~~~~~~~~~
20 |
21 | .. code-block:: python
22 |
23 | # Non-drop-frame
24 | tc = DfttTimecode('01:00:00:00', 'auto', fps=24, drop_frame=False, strict=True)
25 |
26 | # Drop-frame (uses semicolon separator)
27 | tc_df = DfttTimecode('01:00:00;00', 'auto', fps=29.97, drop_frame=True)
28 |
29 | Frame Count
30 | ~~~~~~~~~~~
31 |
32 | .. code-block:: python
33 |
34 | # Using frame count with 'f' suffix
35 | tc = DfttTimecode('1000f', 'auto', fps=24)
36 |
37 | # Using integer (automatically treated as frame count)
38 | tc = DfttTimecode(1000, 'auto', fps=119.88, drop_frame=True)
39 |
40 | Timestamp
41 | ~~~~~~~~~
42 |
43 | .. code-block:: python
44 |
45 | # Using timestamp with 's' suffix
46 | tc = DfttTimecode('3600.0s', 'auto', fps=24)
47 |
48 | # Using float (automatically treated as timestamp)
49 | tc = DfttTimecode(3600.0, 'auto', fps=23.976)
50 |
51 | # Using Fraction for precise frame rates
52 | from fractions import Fraction
53 | tc = DfttTimecode(3600.0, 'auto', fps=Fraction(60000, 1001))
54 |
55 | Other Formats
56 | ~~~~~~~~~~~~~
57 |
58 | .. code-block:: python
59 |
60 | # SRT format
61 | tc = DfttTimecode('01:00:00,000', 'auto', fps=24)
62 |
63 | # FFMPEG format
64 | tc = DfttTimecode('01:00:00.00', 'auto', fps=24)
65 |
66 | # FCPX format
67 | tc = DfttTimecode('1/24s', 'auto', fps=24)
68 |
69 | Accessing Timecode Properties
70 | ------------------------------
71 |
72 | .. code-block:: python
73 |
74 | tc = DfttTimecode('01:00:00:00', 'auto', fps=24)
75 |
76 | print(tc.type) # 'smpte'
77 | print(tc.fps) # 24
78 | print(tc.framecount) # 86400
79 | print(tc.timestamp) # 3600.0
80 | print(tc.is_drop_frame) # False
81 | print(tc.is_strict) # True
82 |
83 | Converting Between Formats
84 | ---------------------------
85 |
86 | .. code-block:: python
87 |
88 | tc = DfttTimecode('01:00:00:00', 'auto', fps=24)
89 |
90 | # Convert to different formats
91 | print(tc.timecode_output('smpte')) # '01:00:00:00'
92 | print(tc.timecode_output('srt')) # '01:00:00,000'
93 | print(tc.timecode_output('ffmpeg')) # '01:00:00.00'
94 | print(tc.timecode_output('fcpx')) # '86400/24s'
95 |
96 | # Get specific parts (1=hours, 2=minutes, 3=seconds, 4=frames/ms)
97 | print(tc.timecode_output('smpte', output_part=1)) # '01'
98 | print(tc.timecode_output('smpte', output_part=4)) # '00'
99 |
100 | Arithmetic Operations
101 | ---------------------
102 |
103 | Adding Timecodes
104 | ~~~~~~~~~~~~~~~~
105 |
106 | .. code-block:: python
107 |
108 | tc1 = DfttTimecode('01:00:00:00', 'auto', fps=24)
109 | tc2 = DfttTimecode('00:30:00:00', 'auto', fps=24)
110 |
111 | result = tc1 + tc2
112 | print(result.timecode_output('smpte')) # '01:30:00:00'
113 |
114 | # Add frames
115 | result = tc1 + 100
116 | print(result.timecode_output('smpte')) # '01:00:04:04'
117 |
118 | # Add seconds
119 | result = tc1 + 60.0
120 | print(result.timecode_output('smpte')) # '01:01:00:00'
121 |
122 | Subtracting Timecodes
123 | ~~~~~~~~~~~~~~~~~~~~~
124 |
125 | .. code-block:: python
126 |
127 | tc1 = DfttTimecode('01:00:00:00', 'auto', fps=24)
128 | tc2 = DfttTimecode('00:30:00:00', 'auto', fps=24)
129 |
130 | result = tc1 - tc2
131 | print(result.timecode_output('smpte')) # '00:30:00:00'
132 |
133 | Multiplying and Dividing
134 | ~~~~~~~~~~~~~~~~~~~~~~~~~
135 |
136 | .. code-block:: python
137 |
138 | tc = DfttTimecode('01:00:00:00', 'auto', fps=24)
139 |
140 | # Multiply
141 | result = tc * 2
142 | print(result.timecode_output('smpte')) # '02:00:00:00'
143 |
144 | # Divide
145 | result = tc / 2
146 | print(result.timecode_output('smpte')) # '00:30:00:00'
147 |
148 | Comparison Operations
149 | ---------------------
150 |
151 | .. code-block:: python
152 |
153 | tc1 = DfttTimecode('01:00:00:00', 'auto', fps=24)
154 | tc2 = DfttTimecode('00:30:00:00', 'auto', fps=24)
155 |
156 | print(tc1 == tc2) # False
157 | print(tc1 != tc2) # True
158 | print(tc1 > tc2) # True
159 | print(tc1 >= tc2) # True
160 | print(tc1 < tc2) # False
161 | print(tc1 <= tc2) # False
162 |
163 | Changing Timecode Properties
164 | -----------------------------
165 |
166 | Changing Frame Rate
167 | ~~~~~~~~~~~~~~~~~~~
168 |
169 | .. code-block:: python
170 |
171 | tc = DfttTimecode('01:00:00:101', 'auto', fps=120)
172 |
173 | # Change FPS with rounding
174 | tc.set_fps(24, rounding=True)
175 | print(tc.timecode_output('smpte')) # Frame-aligned result
176 |
177 | # Change FPS without rounding (preserves timestamp)
178 | tc.set_fps(24, rounding=False)
179 | print(tc.timecode_output('smpte')) # Exact timestamp conversion
180 |
181 | Changing Strict Mode
182 | ~~~~~~~~~~~~~~~~~~~~
183 |
184 | .. code-block:: python
185 |
186 | tc = DfttTimecode('25:01:02:05', 'auto', fps=24, strict=False)
187 | print(tc.timecode_output('smpte')) # '25:01:02:05'
188 |
189 | tc.set_strict(True)
190 | print(tc.timecode_output('smpte')) # '01:01:02:05' (wrapped to 24 hours)
191 |
192 | Changing Timecode Type
193 | ~~~~~~~~~~~~~~~~~~~~~~~
194 |
195 | .. code-block:: python
196 |
197 | tc = DfttTimecode('01:00:00,123', 'auto', fps=24)
198 | print(tc.type) # 'srt'
199 |
200 | tc.set_type('smpte', rounding=True)
201 | print(tc.type) # 'smpte'
202 | print(tc.timecode_output('smpte')) # Frame-aligned SMPTE timecode
203 |
204 | Strict Mode
205 | -----------
206 |
207 | Strict mode ensures timecodes stay within a 24-hour range:
208 |
209 | .. code-block:: python
210 |
211 | # With strict mode enabled (default)
212 | tc = DfttTimecode('25:00:00:00', 'auto', fps=24, strict=True)
213 | print(tc.timecode_output('smpte')) # '01:00:00:00'
214 |
215 | # With strict mode disabled
216 | tc = DfttTimecode('25:00:00:00', 'auto', fps=24, strict=False)
217 | print(tc.timecode_output('smpte')) # '25:00:00:00'
218 |
219 | # Negative values
220 | tc = DfttTimecode('-01:00:00:00', 'auto', fps=24, strict=True)
221 | print(tc.timecode_output('smpte')) # '23:00:00:00'
222 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DFTT Timecode
2 |
3 | [](https://pypi.org/project/dftt-timecode/)
4 | [](https://www.python.org/)
5 | [](https://github.com/OwenYou/dftt_timecode/blob/main/LICENSE)
6 | [](https://owenyou.github.io/dftt_timecode/)
7 |
8 | A high-precision Python timecode library designed for film and TV production.
9 |
10 | 为影视行业设计的高精度Python时码库。
11 |
12 | ## Introduction | 简介
13 |
14 | DFTT Timecode is a professional-grade timecode library for film and television post-production workflows. It provides frame-accurate calculations with support for multiple industry-standard formats, high frame rates, and drop-frame compensation.
15 |
16 | DFTT Timecode是一个专业级的时码库,专为影视后期制作工作流设计。它提供帧精确的计算,支持多种行业标准格式、高帧率和丢帧补偿。
17 |
18 | **DFTT** stands for Department of Film and TV Technology of Beijing Film Academy.
19 |
20 | **DFTT**是北京电影学院影视技术系(Department of Film and TV Technology)的缩写。
21 |
22 | ### Key Features | 主要特性
23 |
24 | - **Multiple Format Support** - SMPTE, SRT, DLP (Cine Canvas), FFMPEG, FCPX, frame count, and timestamps
25 |
26 | **多格式支持** - SMPTE、SRT、DLP(Cine Canvas)、FFMPEG、FCPX、帧计数和时间戳
27 |
28 | - **High Frame Rate** - Supports 0.01 to 999.99 fps
29 |
30 | **高帧率** - 支持0.01至999.99 fps
31 |
32 | - **Drop-Frame Accurate** - Strict SMPTE drop-frame/non-drop-frame compliance
33 |
34 | **丢帧精确** - 严格遵循SMPTE丢帧/非丢帧标准
35 |
36 | - **High Precision** - Uses `fractions.Fraction` internally for lossless calculations
37 |
38 | **高精度** - 内部使用`fractions.Fraction`进行无损计算
39 |
40 | - **Rich Operators** - Full arithmetic (+, -, *, /) and comparison (==, !=, <, >, <=, >=) support
41 |
42 | **丰富的运算符** - 完整的算术和比较运算支持
43 |
44 | - **Strict Mode** - Optional 24-hour wrapping for broadcast workflows
45 |
46 | **严格模式** - 可选的24小时循环模式,适用于广播工作流
47 |
48 | ## Installation | 安装
49 |
50 | ```bash
51 | pip install dftt_timecode
52 | ```
53 |
54 | **Requirements:** Python 3.11 or higher
55 |
56 | **要求:** Python 3.11或更高版本
57 |
58 | ## Quick Start | 快速入门
59 |
60 | ### Basic Usage | 基本使用
61 |
62 | ```python
63 | from dftt_timecode import DfttTimecode
64 |
65 | # Create timecode from SMPTE format
66 | tc = DfttTimecode('01:00:00:00', 'auto', fps=24)
67 | print(tc) # (Timecode:01:00:00:00, Type:smpte, FPS:24.00 NDF, Strict)
68 |
69 | # Access timecode properties
70 | print(tc.framecount) # 86400
71 | print(tc.timestamp) # 3600.0
72 | print(tc.fps) # 24
73 |
74 | # Convert to different formats
75 | print(tc.timecode_output('smpte')) # 01:00:00:00
76 | print(tc.timecode_output('srt')) # 01:00:00,000
77 | print(tc.timecode_output('ffmpeg')) # 01:00:00.00
78 | ```
79 |
80 | ### Arithmetic Operations | 运算操作
81 |
82 | ```python
83 | from dftt_timecode import DfttTimecode
84 |
85 | a = DfttTimecode('01:00:00:00', fps=24)
86 | b = DfttTimecode('00:30:00:00', fps=24)
87 |
88 | # Timecode arithmetic
89 | print(a + b) # 01:30:00:00
90 | print(a - b) # 00:30:00:00
91 | print(a * 2) # 02:00:00:00
92 | print(a / 2) # 00:30:00:00
93 |
94 | # Comparison
95 | print(a > b) # True
96 | print(a == b) # False
97 |
98 | # Add frames (int) or seconds (float)
99 | print(a + 24) # Adds 24 frames
100 | print(a + 1.0) # Adds 1 second
101 | ```
102 |
103 | ### Drop-Frame Timecode | 丢帧时码
104 |
105 | ```python
106 | from dftt_timecode import DfttTimecode
107 |
108 | # Drop-frame timecode for 29.97 fps
109 | df_tc = DfttTimecode('01:00:00;00', fps=29.97, drop_frame=True)
110 | print(df_tc) # Automatically detects drop-frame from semicolon separator
111 |
112 | # Non-drop-frame
113 | ndf_tc = DfttTimecode('01:00:00:00', fps=29.97, drop_frame=False)
114 | ```
115 |
116 | ### Format Conversion | 格式转换
117 |
118 | ```python
119 | from dftt_timecode import DfttTimecode
120 |
121 | tc = DfttTimecode('1000f', fps=24) # Create from frame count
122 |
123 | # Convert to different formats
124 | tc.set_type('smpte')
125 | print(tc.timecode_output()) # 00:00:41:16
126 |
127 | tc.set_type('srt')
128 | print(tc.timecode_output()) # 00:00:41,667
129 |
130 | # Change frame rate
131 | tc.set_fps(25, rounding=True)
132 | print(tc.timecode_output('smpte')) # 00:00:40:00
133 | ```
134 |
135 | ### Using Convenience Aliases | 使用便捷别名
136 |
137 | ```python
138 | # Shorter aliases for quick coding
139 | from dftt_timecode import dtc, dtr
140 |
141 | tc = dtc('01:00:00:00', fps=24) # dtc = DfttTimecode
142 | print(tc.framecount) # 86400
143 | ```
144 |
145 | ## Documentation | 文档
146 |
147 | For comprehensive documentation including detailed API reference, advanced usage, and examples:
148 |
149 | 完整文档包括详细的API参考、高级用法和示例:
150 |
151 | **📖 [View Full Documentation on GitHub Pages](https://owenyou.github.io/dftt_timecode/)**
152 |
153 | ### Documentation Contents | 文档内容
154 |
155 | - **API Reference** - Complete class and method documentation
156 |
157 | **API参考** - 完整的类和方法文档
158 |
159 | - **Advanced Usage** - TimeRange operations, precision handling, format conversion
160 |
161 | **高级用法** - TimeRange操作、精度处理、格式转换
162 |
163 | - **Examples** - Real-world usage patterns and best practices
164 |
165 | **示例** - 实际应用模式和最佳实践
166 |
167 | - **Format Specifications** - Detailed format descriptions and validation rules
168 |
169 | **格式规范** - 详细的格式描述和验证规则
170 |
171 | ## TimeRange Support | 时间范围支持
172 |
173 | The library includes `DfttTimeRange` for working with time intervals:
174 |
175 | 库中包含用于处理时间间隔的`DfttTimeRange`:
176 |
177 | ```python
178 | from dftt_timecode import DfttTimeRange
179 |
180 | # Create a time range
181 | tr = DfttTimeRange(
182 | start='01:00:00:00',
183 | end='02:00:00:00',
184 | fps=24
185 | )
186 |
187 | print(tr.duration) # Duration timecode
188 | print(tr.framecount) # Total frames in range
189 |
190 | # TimeRange operations
191 | tr1 = DfttTimeRange('01:00:00:00', '02:00:00:00', fps=24)
192 | tr2 = DfttTimeRange('01:30:00:00', '02:30:00:00', fps=24)
193 |
194 | intersection = tr1.intersection(tr2) # Overlapping portion
195 | union = tr1.union(tr2) # Combined range
196 | ```
197 |
198 | See the [full documentation](https://owenyou.github.io/dftt_timecode/) for complete TimeRange API reference.
199 |
200 | 查看[完整文档](https://owenyou.github.io/dftt_timecode/)了解完整的TimeRange API参考。
201 |
202 | ## License | 许可证
203 |
204 | This project is licensed under the LGPL 2.1 License - see the [LICENSE](LICENSE) file for details.
205 |
206 | 本项目采用LGPL 2.1许可证 - 详见[LICENSE](LICENSE)文件。
207 |
208 | ## Contributing | 贡献
209 |
210 | Contributions are welcome! Please feel free to submit a Pull Request.
211 |
212 | 欢迎贡献!请随时提交Pull Request。
213 |
214 | ## Links | 链接
215 |
216 | - **PyPI:** https://pypi.org/project/dftt-timecode/
217 | - **Documentation:** https://owenyou.github.io/dftt_timecode/
218 | - **Source Code:** https://github.com/OwenYou/dftt_timecode
219 | - **Issue Tracker:** https://github.com/OwenYou/dftt_timecode/issues
220 |
--------------------------------------------------------------------------------
/docs/locale/zh_CN/LC_MESSAGES/user_guide.po:
--------------------------------------------------------------------------------
1 | # DFTT Timecode Documentation Chinese Translation
2 | # Copyright (C) 2025, You Ziyuan
3 | # This file is distributed under the same license as the DFTT Timecode
4 | # package.
5 | # You Ziyuan , 2025.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: DFTT Timecode 0.0.15a2\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2025-10-21 11:37+0800\n"
12 | "PO-Revision-Date: 2025-10-21 12:10+0800\n"
13 | "Last-Translator: You Ziyuan \n"
14 | "Language: zh_CN\n"
15 | "Language-Team: zh_CN \n"
16 | "Plural-Forms: nplurals=1; plural=0;\n"
17 | "MIME-Version: 1.0\n"
18 | "Content-Type: text/plain; charset=utf-8\n"
19 | "Content-Transfer-Encoding: 8bit\n"
20 | "Generated-By: Babel 2.17.0\n"
21 |
22 | #: ../../user_guide.rst:2
23 | msgid "User Guide"
24 | msgstr "用户指南"
25 |
26 | #: ../../user_guide.rst:4
27 | msgid "This comprehensive guide covers all aspects of using dftt_timecode."
28 | msgstr "本综合指南涵盖了使用 dftt_timecode 的各个方面。"
29 |
30 | #: ../../user_guide.rst:7
31 | msgid "Overview"
32 | msgstr "概述"
33 |
34 | #: ../../user_guide.rst:9
35 | msgid ""
36 | "dftt_timecode is a comprehensive Python library designed for the film and"
37 | " TV industry to handle timecodes in various formats with high precision. "
38 | "It supports high frame rates (HFR) up to 999.99 fps and provides a rich "
39 | "set of operations for timecode manipulation."
40 | msgstr "dftt_timecode 是一个为影视行业设计的综合性 Python 库,用于高精度处理各种格式的时码。它支持高达 999.99 fps 的高帧率(HFR),并提供丰富的时码操作功能。"
41 |
42 | #: ../../user_guide.rst:14
43 | msgid "Key Concepts"
44 | msgstr "核心概念"
45 |
46 | #: ../../user_guide.rst:17
47 | msgid "Timecode Types"
48 | msgstr "时码类型"
49 |
50 | #: ../../user_guide.rst:19
51 | msgid "The library supports multiple timecode formats used in different contexts:"
52 | msgstr "该库支持在不同场景下使用的多种时码格式:"
53 |
54 | #: ../../user_guide.rst:21
55 | msgid "**SMPTE**: Industry-standard format (HH:MM:SS:FF)"
56 | msgstr "**SMPTE**:行业标准格式(HH:MM:SS:FF)"
57 |
58 | #: ../../user_guide.rst:22
59 | msgid "**SRT**: SubRip subtitle format (HH:MM:SS,mmm)"
60 | msgstr "**SRT**:SubRip 字幕格式(HH:MM:SS,mmm)"
61 |
62 | #: ../../user_guide.rst:23
63 | msgid "**FFMPEG**: FFmpeg timestamp format (HH:MM:SS.ff)"
64 | msgstr "**FFMPEG**:FFmpeg 时间戳格式(HH:MM:SS.ff)"
65 |
66 | #: ../../user_guide.rst:24
67 | msgid "**FCPX**: Final Cut Pro X format (frames/fps)"
68 | msgstr "**FCPX**:Final Cut Pro X 格式(frames/fps)"
69 |
70 | #: ../../user_guide.rst:25
71 | msgid "**DLP**: Digital cinema format (HH:MM:SS:FFF)"
72 | msgstr "**DLP**:数字电影格式(HH:MM:SS:FFF)"
73 |
74 | #: ../../user_guide.rst:26
75 | msgid "**Frame**: Simple frame count"
76 | msgstr "**Frame**:简单的帧计数"
77 |
78 | #: ../../user_guide.rst:27
79 | msgid "**Time**: Seconds-based timestamp"
80 | msgstr "**Time**:基于秒的时间戳"
81 |
82 | #: ../../user_guide.rst:30
83 | msgid "Frame Rates"
84 | msgstr "帧率"
85 |
86 | #: ../../user_guide.rst:32
87 | msgid "The library supports:"
88 | msgstr "该库支持:"
89 |
90 | #: ../../user_guide.rst:34
91 | msgid "Standard frame rates (23.976, 24, 25, 29.97, 30, 50, 59.94, 60, etc.)"
92 | msgstr "标准帧率(23.976、24、25、29.97、30、50、59.94、60 等)"
93 |
94 | #: ../../user_guide.rst:35
95 | msgid "High frame rates (96, 100, 120, 144, 240, etc.)"
96 | msgstr "高帧率(96、100、120、144、240 等)"
97 |
98 | #: ../../user_guide.rst:36
99 | msgid "Custom frame rates from 0.01 to 999.99 fps"
100 | msgstr "0.01 到 999.99 fps 的自定义帧率"
101 |
102 | #: ../../user_guide.rst:37
103 | msgid "Precise fractional frame rates using Python's Fraction type"
104 | msgstr "使用 Python 的 Fraction 类型的精确分数帧率"
105 |
106 | #: ../../user_guide.rst:40
107 | msgid "Drop-Frame vs Non-Drop-Frame"
108 | msgstr "跳帧 vs 非跳帧"
109 |
110 | #: ../../user_guide.rst:42
111 | msgid ""
112 | "For NTSC video standards (29.97 fps, 59.94 fps), the library correctly "
113 | "handles:"
114 | msgstr "对于 NTSC 视频标准(29.97 fps、59.94 fps),该库正确处理:"
115 |
116 | #: ../../user_guide.rst:44
117 | msgid "Non-drop-frame (NDF): Uses colon separator (HH:MM:SS:FF)"
118 | msgstr "非跳帧(NDF):使用冒号分隔符(HH:MM:SS:FF)"
119 |
120 | #: ../../user_guide.rst:45
121 | msgid "Drop-frame (DF): Uses semicolon separator (HH:MM:SS;FF)"
122 | msgstr "跳帧(DF):使用分号分隔符(HH:MM:SS;FF)"
123 |
124 | #: ../../user_guide.rst:47
125 | msgid ""
126 | "Drop-frame timecode compensates for the slight discrepancy between "
127 | "nominal and actual frame rates by periodically skipping frame numbers."
128 | msgstr "跳帧时码通过定期跳过帧编号来补偿名义帧率和实际帧率之间的微小差异。"
129 |
130 | #: ../../user_guide.rst:51
131 | msgid "Strict Mode"
132 | msgstr "严格模式"
133 |
134 | #: ../../user_guide.rst:53
135 | msgid "Strict mode ensures timecodes remain within a 24-hour cycle:"
136 | msgstr "严格模式确保时码保持在 24 小时周期内:"
137 |
138 | #: ../../user_guide.rst:55
139 | msgid ""
140 | "Enabled: Timecodes wrap around at 24 hours (25:00:00:00 becomes "
141 | "01:00:00:00)"
142 | msgstr "启用:时码在 24 小时处循环(25:00:00:00 变为 01:00:00:00)"
143 |
144 | #: ../../user_guide.rst:56
145 | msgid "Disabled: Timecodes can exceed 24 hours (useful for long-form content)"
146 | msgstr "禁用:时码可以超过 24 小时(适用于长篇内容)"
147 |
148 | #: ../../user_guide.rst:59
149 | msgid "Common Use Cases"
150 | msgstr "常见使用场景"
151 |
152 | #: ../../user_guide.rst:62
153 | msgid "Video Editing"
154 | msgstr "视频编辑"
155 |
156 | #: ../../user_guide.rst:77
157 | msgid "Subtitle Timing"
158 | msgstr "字幕时间轴"
159 |
160 | #: ../../user_guide.rst:90
161 | msgid "Frame Rate Conversion"
162 | msgstr "帧率转换"
163 |
164 | #: ../../user_guide.rst:100
165 | msgid "High Frame Rate Workflows"
166 | msgstr "高帧率工作流"
167 |
168 | #: ../../user_guide.rst:112
169 | msgid "Best Practices"
170 | msgstr "最佳实践"
171 |
172 | #: ../../user_guide.rst:114
173 | msgid ""
174 | "**Use Strict Mode for Standard Workflows**: Enable strict mode for "
175 | "typical video editing to prevent timecode values from exceeding 24 hours."
176 | msgstr "**标准工作流使用严格模式**:为典型的视频编辑启用严格模式,以防止时码值超过 24 小时。"
177 |
178 | #: ../../user_guide.rst:117
179 | msgid ""
180 | "**Specify Frame Rates Explicitly**: Always specify the correct frame rate"
181 | " when creating timecode objects to ensure accurate conversions."
182 | msgstr "**明确指定帧率**:创建时码对象时始终指定正确的帧率,以确保准确转换。"
183 |
184 | #: ../../user_guide.rst:120
185 | msgid ""
186 | "**Use Fraction for Precise Frame Rates**: For frame rates like 23.976 or "
187 | "29.97, use Fraction for maximum precision:"
188 | msgstr "**使用 Fraction 获得精确帧率**:对于 23.976 或 29.97 等帧率,使用 Fraction 以获得最大精度:"
189 |
190 | #: ../../user_guide.rst:128
191 | msgid ""
192 | "**Handle Drop-Frame Correctly**: When working with NTSC frame rates "
193 | "(29.97, 59.94), ensure drop-frame is set correctly based on your workflow"
194 | " requirements."
195 | msgstr "**正确处理跳帧**:使用 NTSC 帧率(29.97、59.94)时,根据工作流要求确保正确设置跳帧。"
196 |
197 | #: ../../user_guide.rst:131
198 | msgid ""
199 | "**Validate User Input**: Use try-except blocks to catch and handle "
200 | "timecode errors gracefully:"
201 | msgstr "**验证用户输入**:使用 try-except 块优雅地捕获和处理时码错误:"
202 |
203 | #: ../../user_guide.rst:143
204 | msgid "Performance Considerations"
205 | msgstr "性能考虑"
206 |
207 | #: ../../user_guide.rst:145
208 | msgid ""
209 | "The library uses high-precision Fraction internally for timestamp "
210 | "storage, which ensures accuracy but may be slower than floating-point "
211 | "arithmetic. For performance-critical applications:"
212 | msgstr "该库在内部使用高精度 Fraction 存储时间戳,这确保了准确性,但可能比浮点运算慢。对于性能关键型应用:"
213 |
214 | #: ../../user_guide.rst:148
215 | msgid "Create timecode objects once and reuse them"
216 | msgstr "创建一次时码对象并重复使用"
217 |
218 | #: ../../user_guide.rst:149
219 | msgid "Use frame count operations when possible (integer arithmetic is faster)"
220 | msgstr "尽可能使用帧计数操作(整数运算更快)"
221 |
222 | #: ../../user_guide.rst:150
223 | msgid ""
224 | "Consider caching converted values if the same conversions are needed "
225 | "repeatedly"
226 | msgstr "如果需要重复进行相同的转换,请考虑缓存转换后的值"
227 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ## [1.0.0b3]
11 |
12 | ### Fixed
13 |
14 | - Fix Python 3.14 compatibility issue in `move_frame()` method where `Fraction()` constructor with two arguments failed due to stricter type requirements
15 |
16 | 修复 Python 3.14 兼容性问题,`move_frame()` 方法中 `Fraction()` 构造函数因更严格的类型要求而失败
17 |
18 | - Fix precision loss issues in timecode initialization methods (`__init_smpte()`, `__init_frame()`) by using `Fraction()` division instead of float division
19 |
20 | 修复时码初始化方法中的精度损失问题,使用 `Fraction()` 除法替代浮点数除法
21 |
22 | - Fix FCPX format output to correctly handle integer timestamps, avoiding large numerator values when the result is a whole number
23 |
24 | 修复 FCPX 格式输出,正确处理整数时间戳,避免结果为整数时出现大分子值
25 |
26 | ### Added
27 |
28 | - Add `move_frame()` and `move_time()` methods to DfttTimecode class for moving timecode by specified frames or time duration
29 |
30 | 为 DfttTimecode 类添加 `move_frame()` 和 `move_time()` 方法,用于按指定帧数或时间段移动时码
31 |
32 | - Add comprehensive unit tests (32 tests) for `move_frame()` and `move_time()` methods
33 |
34 | 为 `move_frame()` 和 `move_time()` 方法添加全面的单元测试(32 个测试)
35 |
36 | - Frame movement tests: forward/backward movement, zero movement, large movements (86400 frames)
37 |
38 | 帧移动测试:正向/反向移动、零移动、大规模移动(86400 帧)
39 |
40 | - Time movement tests: float/int/Fraction input support, various time movements
41 |
42 | 时间移动测试:浮点数/整数/分数输入支持、各种时间移动
43 |
44 | - Strict mode tests: 24-hour cycling behavior verification
45 |
46 | 严格模式测试:24 小时循环行为验证
47 |
48 | - Different frame rates: 24, 30, 60, 119.88 fps
49 |
50 | 不同帧率:24、30、60、119.88 fps
51 |
52 | - Drop-frame timecode support tests
53 |
54 | 丢帧时码支持测试
55 |
56 | - Method chaining and input validation tests
57 |
58 | 方法链和输入验证测试
59 |
60 | - Equivalence test between `move_frame()` and `move_time()`
61 |
62 | `move_frame()` 和 `move_time()` 的等价性测试
63 |
64 | ### Changed
65 |
66 | - Improved internal timestamp precision across the entire codebase by consistently using `Fraction()` arithmetic
67 |
68 | 通过始终使用 `Fraction()` 算术运算,提高整个代码库的内部时间戳精度
69 |
70 | ## [1.0.0b2]
71 | ### Fixed
72 | - Fix bug for fcpx output format
73 | 修复 fcpx 输出格式的错误
74 |
75 | ## [1.0.0b1]
76 |
77 | ### Summary
78 |
79 | First beta release! The library has reached feature completeness and API stability for its core functionality. All major features are implemented, tested, and documented. This beta release is ready for production testing and feedback.
80 |
81 | ### Added
82 |
83 | - Comprehensive internationalization (i18n) support with Chinese translations
84 |
85 | 完整的国际化支持,包含中文翻译
86 |
87 | - Language switcher in documentation
88 |
89 | 文档语言切换器
90 |
91 | - Enhanced documentation with auto-generated changelog integration
92 |
93 | 增强文档,自动集成 CHANGELOG.md
94 |
95 | ### Changed
96 |
97 | - API is now considered stable for 1.0.0 release
98 |
99 | API 现在被视为稳定版本,准备发布 1.0.0
100 |
101 | - Updated development status from Pre-Alpha to Beta
102 |
103 | 开发状态从预览版更新为测试版
104 |
105 | ### Core Features (Stable)
106 |
107 | - Multiple timecode format support: SMPTE (DF/NDF), SRT, FFMPEG, FCPX, DLP, frame count, timestamps
108 |
109 | 多种时码格式支持
110 |
111 | - High frame rate support (0.01-999.99 fps)
112 |
113 | 高帧率支持
114 |
115 | - High-precision calculations using Fraction for lossless accuracy
116 |
117 | 使用 Fraction 进行高精度计算
118 |
119 | - Full arithmetic operators (+, -, *, /) and comparison operators (==, !=, <, >, <=, >=)
120 |
121 | 完整的算术和比较运算符
122 |
123 | - DfttTimeRange class for time interval operations
124 |
125 | 时间范围操作类
126 |
127 | - Comprehensive test coverage with pytest
128 |
129 | 完整的测试覆盖
130 |
131 | - Sphinx-based documentation with API reference
132 |
133 | 基于 Sphinx 的文档和 API 参考
134 |
135 | - Automated CI/CD for documentation and PyPI publishing
136 |
137 | 自动化 CI/CD 用于文档和 PyPI 发布
138 |
139 | ## [0.0.15a2] - Pre-Alpha
140 |
141 | ### Fixed
142 |
143 | - Fix several error string missing user input logging during format
144 |
145 | 修复遗漏的错误信息格式化 [#26](https://github.com/OwenYou/dftt_timecode/issues/26)
146 |
147 | ## [0.0.15a1] - Pre-Alpha
148 |
149 | ### Added
150 |
151 | - Add `DfttTimecodeInitializationError` exception class
152 |
153 | 增加 `DfttTimecodeInitializationError`
154 |
155 | - Add GitHub Action to automatically upload releases to PyPI
156 |
157 | 使用 GitHub Action 自动打包上传 PyPI
158 |
159 | ### Changed
160 |
161 | - Modify drop frame logic to adapt to more frame rates
162 |
163 | 修改丢帧逻辑适应更多帧率
164 |
165 | - Refactor timecode type detection logic
166 |
167 | 重构时码类型检测逻辑
168 |
169 | - Refactor error handling during timecode initialization
170 |
171 | 重构时码对象初始化时的错误处理
172 |
173 | - Modify error messages to enhance readability
174 |
175 | 修改报错信息,增强可读性
176 |
177 | ### Deprecated
178 |
179 | - Completely remove `InstanceMethodDispatch`
180 |
181 | 完全移除 `InstanceMethodDispatch`
182 |
183 | ### Fixed
184 |
185 | - Fix millisecond overflow issue when converting certain timecodes from SMPTE to SRT [#19](https://github.com/OwenYou/dftt_timecode/issues/19)
186 |
187 | 修复特定时码在SMPTE转SRT时毫秒会溢出的问题
188 |
189 | ## [0.0.14]
190 |
191 | ### Fixed
192 |
193 | - Fix v0.0.13 import failure caused by missing dftt_timecode.core while packing
194 |
195 | 修复v0.0.13打包后不包含core,导致库无法使用的问题
196 |
197 | ## [0.0.13]
198 |
199 | ### Added
200 |
201 | - Add `get_audio_sample_count` method for correctly outputting the count of audio samples at TC timestamps [#9](https://github.com/OwenYou/dftt_timecode/issues/9)
202 |
203 | 添加 `get_audio_sample_count` 方法用于正确输出TC时间戳下的音频采样数
204 |
205 | ### Changed
206 |
207 | - Use f-string for string format output
208 |
209 | 使用 `f-string` 处理字符串格式输出
210 |
211 | - Refactor timecode output function to reduce code duplication
212 |
213 | 重构时间码输出函数,减少代码重复
214 |
215 | ### Deprecated
216 |
217 | - Use `functools.singledispatchmethod` instead of `dispatch.InstanceMethodDispatch`
218 |
219 | 使用 `functools.singledispatchmethod` 代替 `dispatch.InstanceMethodDispatch`
220 |
221 | ## [0.0.12]
222 |
223 | ### Fixed
224 |
225 | - Fix DLP pattern error causing DLP ticks in range [50-99] and [150-199] to not be matched. This bug caused errors when using strings like `'00:00:27:183'` to initialize a DLP timecode object
226 |
227 | 修复DLP正则表达式错误导致范围在50-99、150-199的DLP tick不能被匹配的问题
228 |
229 | ## [0.0.11]
230 |
231 | ### Added
232 |
233 | - Add `__str__` method to return timecode value for DfttTimecode object
234 |
235 | 添加 `__str__` 方法,返回DfttTimecode对象的时间码值
236 |
237 | - Add unit tests for DfttTimecode using pytest
238 |
239 | 添加DfttTimecode单元测试(使用pytest)
240 |
241 | ### Changed
242 |
243 | - Add 23.98/23.976 FPS to drop frame detection conditions
244 |
245 | 对丢帧的检测条件添加有关23.98/23.976的判定
246 |
247 | - `+` and `-` operators perform an OR operation on the strict property of two DfttTimecode objects being added/subtracted
248 |
249 | `+` `-` 运算符对相加的两个DfttTimecode对象的strict属性进行或运算
250 |
251 | - Comparison operators (`==`, `>`, `>=`, etc.) now validate that both DfttTimecode objects have the same frame rate before comparison, throwing an exception if frame rates differ
252 |
253 | 比较运算符在对两个DfttTimecode对象进行比较时会先判定帧率,若帧率不同则抛出异常
254 |
255 | - `print(self)` now outputs a formatted timecode string
256 |
257 | `print(self)` 将会输出基于类型的时间码字符串
258 |
259 | ### Fixed
260 |
261 | - Fix issue in `timecode_output(self, dest_type, output_part)` where `output_part = 3` would incorrectly return minute data
262 |
263 | 修复 `timecode_output` 中 `output_part = 3` 时错误返回分钟数据的问题
264 |
265 | ## [0.0.10]
266 |
267 | ### Added
268 |
269 | - Add support for using a DfttTimecode object to initialize a new DfttTimecode object
270 |
271 | 使用DfttTimecode对象初始化新DfttTimecode对象
272 |
273 | - Add `float()` and `int()` class methods for DfttTimecode class
274 |
275 | 添加DfttTimecode类的float和int方法
276 |
277 | - Add `precise_timestamp` attribute for DfttTimecode class
278 |
279 | 添加DfttTimecode类的precise_timestamp属性
280 |
281 | ### Changed
282 |
283 | - DfttTimecode operators now raise errors when encountering undefined circumstances or illegal operations
284 |
285 | DfttTimecode运算符在未定义/非法操作时将会报错
286 |
287 | - Update comparison rules for DfttTimecode operators
288 |
289 | 修改DfttTimecode运算符的大小比较规则
290 |
291 | - When creating a timecode object using SMPTE NDF format string, if `drop_frame` is forced to `True`, the resulting object will be SMPTE DF format timecode
292 |
293 | 使用SMPTE NDF格式字符串新建时码类对象时,若强制drop_frame为True,则新建得到的对象为SMPTE DF格式时码
294 |
295 | ## [0.0.9]
296 |
297 | Initial public release.
298 |
--------------------------------------------------------------------------------
/docs/locale/zh_CN/LC_MESSAGES/api/error.po:
--------------------------------------------------------------------------------
1 | # DFTT Timecode Documentation Chinese Translation - API Error Module
2 | # Copyright (C) 2025, You Ziyuan
3 | # This file is distributed under the same license as the DFTT Timecode
4 | # package.
5 | # You Ziyuan , 2025.
6 | #
7 | # Note: API documentation keeps technical details in English for accuracy
8 | # Only translating main descriptions and page titles
9 | msgid ""
10 | msgstr ""
11 | "Project-Id-Version: DFTT Timecode 0.0.15a2\n"
12 | "Report-Msgid-Bugs-To: \n"
13 | "POT-Creation-Date: 2025-10-21 11:37+0800\n"
14 | "PO-Revision-Date: 2025-10-21 12:25+0800\n"
15 | "Last-Translator: You Ziyuan \n"
16 | "Language: zh_CN\n"
17 | "Language-Team: zh_CN \n"
18 | "Plural-Forms: nplurals=1; plural=0;\n"
19 | "MIME-Version: 1.0\n"
20 | "Content-Type: text/plain; charset=utf-8\n"
21 | "Content-Transfer-Encoding: 8bit\n"
22 | "Generated-By: Babel 2.17.0\n"
23 |
24 | #: ../../api/error.rst:2
25 | msgid "Error Handling"
26 | msgstr "错误处理"
27 |
28 | #: ../../api/error.rst:7
29 | msgid "Exception Classes"
30 | msgstr "异常类"
31 |
32 | #: ../../api/error.rst:9
33 | msgid ""
34 | "The dftt_timecode library defines custom exception classes for different "
35 | "error conditions."
36 | msgstr "dftt_timecode 库为不同的错误情况定义了自定义异常类。"
37 |
38 | #: ../../api/error.rst:12
39 | msgid "DFTTError"
40 | msgstr ""
41 |
42 | #: dftt_timecode.error.DFTTError:1 of
43 | msgid "Bases: :py:class:`Exception`"
44 | msgstr ""
45 |
46 | #: dftt_timecode.error.DFTTError:1 of
47 | #, fuzzy
48 | msgid "Base exception class for all DFTT Timecode library errors."
49 | msgstr "所有 dftt_timecode 错误的基础异常类。"
50 |
51 | #: dftt_timecode.error.DFTTError:3 of
52 | msgid "All custom exceptions in this library inherit from this base class."
53 | msgstr ""
54 |
55 | #: ../../api/error.rst:18
56 | msgid "Base exception class for all dftt_timecode errors."
57 | msgstr "所有 dftt_timecode 错误的基础异常类。"
58 |
59 | #: ../../api/error.rst:21
60 | msgid "DFTTTimecodeValueError"
61 | msgstr ""
62 |
63 | #: dftt_timecode.error.DFTTTimeRangeFPSError:1
64 | #: dftt_timecode.error.DFTTTimeRangeMethodError:1
65 | #: dftt_timecode.error.DFTTTimeRangeTypeError:1
66 | #: dftt_timecode.error.DFTTTimeRangeValueError:1
67 | #: dftt_timecode.error.DFTTTimecodeInitializationError:1
68 | #: dftt_timecode.error.DFTTTimecodeOperatorError:1
69 | #: dftt_timecode.error.DFTTTimecodeTypeError:1
70 | #: dftt_timecode.error.DFTTTimecodeValueError:1 of
71 | #, fuzzy
72 | msgid "Bases: :py:class:`~dftt_timecode.error.DFTTError`"
73 | msgstr "所有 dftt_timecode 错误的基础异常类。"
74 |
75 | #: dftt_timecode.error.DFTTTimecodeValueError:1 of
76 | #, fuzzy
77 | msgid "Raised when a timecode value is invalid or out of acceptable range."
78 | msgstr "当提供无效的时码值时抛出。"
79 |
80 | #: dftt_timecode.error.DFTTTimeRangeFPSError:3
81 | #: dftt_timecode.error.DFTTTimeRangeMethodError:3
82 | #: dftt_timecode.error.DFTTTimeRangeTypeError:3
83 | #: dftt_timecode.error.DFTTTimeRangeValueError:3
84 | #: dftt_timecode.error.DFTTTimecodeInitializationError:3
85 | #: dftt_timecode.error.DFTTTimecodeOperatorError:3
86 | #: dftt_timecode.error.DFTTTimecodeTypeError:3
87 | #: dftt_timecode.error.DFTTTimecodeValueError:3 of
88 | msgid "Examples include:"
89 | msgstr ""
90 |
91 | #: dftt_timecode.error.DFTTTimecodeValueError:4 of
92 | msgid "Frame number exceeding the frame rate limit"
93 | msgstr ""
94 |
95 | #: dftt_timecode.error.DFTTTimecodeValueError:5 of
96 | msgid "Invalid drop-frame timecode values"
97 | msgstr ""
98 |
99 | #: dftt_timecode.error.DFTTTimecodeValueError:6 of
100 | msgid "Illegal timecode values for the given parameters"
101 | msgstr ""
102 |
103 | #: ../../api/error.rst:27
104 | msgid "Raised when an invalid timecode value is provided."
105 | msgstr "当提供无效的时码值时抛出。"
106 |
107 | #: ../../api/error.rst:30
108 | msgid "DFTTTimecodeInitializationError"
109 | msgstr ""
110 |
111 | #: ../../api/error.rst:36 dftt_timecode.error.DFTTTimecodeInitializationError:1
112 | #: of
113 | msgid "Raised when timecode initialization fails due to incompatible parameters."
114 | msgstr ""
115 |
116 | #: dftt_timecode.error.DFTTTimecodeInitializationError:4 of
117 | msgid "Drop-frame status mismatch with timecode format"
118 | msgstr ""
119 |
120 | #: dftt_timecode.error.DFTTTimecodeInitializationError:5 of
121 | msgid "Incompatible timecode value and type combinations"
122 | msgstr ""
123 |
124 | #: ../../api/error.rst:39
125 | msgid "DFTTTimecodeTypeError"
126 | msgstr ""
127 |
128 | #: dftt_timecode.error.DFTTTimecodeTypeError:1 of
129 | #, fuzzy
130 | msgid "Raised when a timecode type is invalid or incompatible."
131 | msgstr "当提供无效的时码值时抛出。"
132 |
133 | #: dftt_timecode.error.DFTTTimecodeTypeError:4 of
134 | msgid "Unknown timecode format type"
135 | msgstr ""
136 |
137 | #: dftt_timecode.error.DFTTTimecodeTypeError:5 of
138 | msgid "Type mismatch between expected and actual timecode format"
139 | msgstr ""
140 |
141 | #: dftt_timecode.error.DFTTTimecodeTypeError:6 of
142 | msgid "Invalid data type for timecode operations"
143 | msgstr ""
144 |
145 | #: ../../api/error.rst:45
146 | #, fuzzy
147 | msgid "Raised when an invalid timecode type is specified or type mismatch occurs."
148 | msgstr "当提供无效的时码值时抛出。"
149 |
150 | #: ../../api/error.rst:48
151 | msgid "DFTTTimecodeOperatorError"
152 | msgstr ""
153 |
154 | #: ../../api/error.rst:54 dftt_timecode.error.DFTTTimecodeOperatorError:1 of
155 | msgid ""
156 | "Raised when an arithmetic or comparison operation on timecode objects "
157 | "fails."
158 | msgstr ""
159 |
160 | #: dftt_timecode.error.DFTTTimecodeOperatorError:4 of
161 | msgid "Operations between timecodes with different frame rates"
162 | msgstr ""
163 |
164 | #: dftt_timecode.error.DFTTTimecodeOperatorError:5 of
165 | msgid "Undefined operations (e.g., dividing number by timecode)"
166 | msgstr ""
167 |
168 | #: dftt_timecode.error.DFTTTimecodeOperatorError:6 of
169 | msgid "Invalid operand types for timecode arithmetic"
170 | msgstr ""
171 |
172 | #: ../../api/error.rst:57
173 | msgid "DFTTTimeRangeMethodError"
174 | msgstr ""
175 |
176 | #: ../../api/error.rst:63 dftt_timecode.error.DFTTTimeRangeMethodError:1 of
177 | msgid ""
178 | "Raised when a timerange method is called with invalid parameters or "
179 | "conditions."
180 | msgstr ""
181 |
182 | #: dftt_timecode.error.DFTTTimeRangeMethodError:4 of
183 | msgid "Attempting to intersect/union timeranges with different directions"
184 | msgstr ""
185 |
186 | #: dftt_timecode.error.DFTTTimeRangeMethodError:5 of
187 | msgid "Invalid offset or extend values"
188 | msgstr ""
189 |
190 | #: dftt_timecode.error.DFTTTimeRangeMethodError:6 of
191 | msgid "Operations on non-overlapping, non-adjacent timeranges"
192 | msgstr ""
193 |
194 | #: ../../api/error.rst:66
195 | msgid "DFTTTimeRangeValueError"
196 | msgstr ""
197 |
198 | #: ../../api/error.rst:72 dftt_timecode.error.DFTTTimeRangeValueError:1 of
199 | msgid "Raised when a timerange value is invalid or out of acceptable range."
200 | msgstr ""
201 |
202 | #: dftt_timecode.error.DFTTTimeRangeValueError:4 of
203 | msgid "Zero-length timerange"
204 | msgstr ""
205 |
206 | #: dftt_timecode.error.DFTTTimeRangeValueError:5 of
207 | msgid "Duration exceeding 24 hours in strict mode"
208 | msgstr ""
209 |
210 | #: dftt_timecode.error.DFTTTimeRangeValueError:6 of
211 | msgid "Invalid timerange separation parameters"
212 | msgstr ""
213 |
214 | #: ../../api/error.rst:75
215 | msgid "DFTTTimeRangeTypeError"
216 | msgstr ""
217 |
218 | #: ../../api/error.rst:81 dftt_timecode.error.DFTTTimeRangeTypeError:1 of
219 | msgid "Raised when a timerange type or operand type is invalid."
220 | msgstr ""
221 |
222 | #: dftt_timecode.error.DFTTTimeRangeTypeError:4 of
223 | msgid "Invalid item type for contains check"
224 | msgstr ""
225 |
226 | #: dftt_timecode.error.DFTTTimeRangeTypeError:5 of
227 | msgid "Attempting to operate on non-timerange objects"
228 | msgstr ""
229 |
230 | #: dftt_timecode.error.DFTTTimeRangeTypeError:6 of
231 | msgid "Type mismatches in timerange operations"
232 | msgstr ""
233 |
234 | #: ../../api/error.rst:84
235 | msgid "DFTTTimeRangeFPSError"
236 | msgstr ""
237 |
238 | #: ../../api/error.rst:90 dftt_timecode.error.DFTTTimeRangeFPSError:1 of
239 | msgid "Raised when timerange operations fail due to frame rate mismatches."
240 | msgstr ""
241 |
242 | #: dftt_timecode.error.DFTTTimeRangeFPSError:4 of
243 | msgid "FPS mismatch between start and end timecodes"
244 | msgstr ""
245 |
246 | #: dftt_timecode.error.DFTTTimeRangeFPSError:5 of
247 | msgid "Operations between timeranges with different frame rates"
248 | msgstr ""
249 |
250 | #: ../../api/error.rst:93
251 | msgid "Error Examples"
252 | msgstr ""
253 |
254 | #: ../../api/error.rst:96
255 | #, fuzzy
256 | msgid "Invalid Timecode Value"
257 | msgstr "当提供无效的时码值时抛出。"
258 |
259 | #: ../../api/error.rst:109
260 | msgid "Timecode Initialization Error"
261 | msgstr ""
262 |
263 | #: ../../api/error.rst:123
264 | msgid "Type Error"
265 | msgstr ""
266 |
267 | #: ../../api/error.rst:136
268 | msgid "Operator Error"
269 | msgstr ""
270 |
271 |
--------------------------------------------------------------------------------
/docs/locale/zh_CN/LC_MESSAGES/changelog.po:
--------------------------------------------------------------------------------
1 | # DFTT Timecode Documentation Chinese Translation
2 | # Copyright (C) 2025, You Ziyuan
3 | # This file is distributed under the same license as the DFTT Timecode
4 | # package.
5 | # You Ziyuan , 2025.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: DFTT Timecode 0.0.15a2\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2025-10-21 11:37+0800\n"
12 | "PO-Revision-Date: 2025-10-21 12:20+0800\n"
13 | "Last-Translator: You Ziyuan \n"
14 | "Language: zh_CN\n"
15 | "Language-Team: zh_CN \n"
16 | "Plural-Forms: nplurals=1; plural=0;\n"
17 | "MIME-Version: 1.0\n"
18 | "Content-Type: text/plain; charset=utf-8\n"
19 | "Content-Transfer-Encoding: 8bit\n"
20 | "Generated-By: Babel 2.17.0\n"
21 |
22 | #: ../../../CHANGELOG.md:1
23 | msgid "Changelog"
24 | msgstr "更新日志"
25 |
26 | #: ../../../CHANGELOG.md:3
27 | msgid "All notable changes to this project will be documented in this file."
28 | msgstr "此项目的所有重要更改都将记录在此文件中。"
29 |
30 | #: ../../../CHANGELOG.md:5
31 | msgid ""
32 | "The format is based on [Keep a "
33 | "Changelog](https://keepachangelog.com/en/1.0.0/), and this project "
34 | "adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)."
35 | msgstr ""
36 | "格式基于 [Keep a "
37 | "Changelog](https://keepachangelog.com/en/1.0.0/),本项目遵循[语义化版本控制](https://semver.org/spec/v2.0.0.html)。"
38 |
39 | #: ../../../CHANGELOG.md:8
40 | msgid "[Unreleased]"
41 | msgstr ""
42 |
43 | #: ../../../CHANGELOG.md:10
44 | msgid "[0.0.15a2] - Pre-Alpha"
45 | msgstr ""
46 |
47 | #: ../../../CHANGELOG.md:12 ../../../CHANGELOG.md:54 ../../../CHANGELOG.md:62
48 | #: ../../../CHANGELOG.md:94 ../../../CHANGELOG.md:130
49 | msgid "Fixed"
50 | msgstr ""
51 |
52 | #: ../../../CHANGELOG.md:14
53 | msgid "Fix several error string missing user input logging during format"
54 | msgstr ""
55 |
56 | #: ../../../CHANGELOG.md:16
57 | msgid "修复遗漏的错误信息格式化 [#26](https://github.com/OwenYou/dftt_timecode/issues/26)"
58 | msgstr ""
59 |
60 | #: ../../../CHANGELOG.md:18
61 | msgid "[0.0.15a1] - Pre-Alpha"
62 | msgstr ""
63 |
64 | #: ../../../CHANGELOG.md:20 ../../../CHANGELOG.md:70 ../../../CHANGELOG.md:102
65 | #: ../../../CHANGELOG.md:138
66 | msgid "Added"
67 | msgstr ""
68 |
69 | #: ../../../CHANGELOG.md:22
70 | msgid "Add `DfttTimecodeInitializationError` exception class"
71 | msgstr ""
72 |
73 | #: ../../../CHANGELOG.md:24
74 | msgid "增加 `DfttTimecodeInitializationError`"
75 | msgstr ""
76 |
77 | #: ../../../CHANGELOG.md:26
78 | msgid "Add GitHub Action to automatically upload releases to PyPI"
79 | msgstr ""
80 |
81 | #: ../../../CHANGELOG.md:28
82 | msgid "使用 GitHub Action 自动打包上传 PyPI"
83 | msgstr ""
84 |
85 | #: ../../../CHANGELOG.md:30 ../../../CHANGELOG.md:76 ../../../CHANGELOG.md:112
86 | #: ../../../CHANGELOG.md:152
87 | #, fuzzy
88 | msgid "Changed"
89 | msgstr "更新日志"
90 |
91 | #: ../../../CHANGELOG.md:32
92 | msgid "Modify drop frame logic to adapt to more frame rates"
93 | msgstr ""
94 |
95 | #: ../../../CHANGELOG.md:34
96 | msgid "修改丢帧逻辑适应更多帧率"
97 | msgstr ""
98 |
99 | #: ../../../CHANGELOG.md:36
100 | msgid "Refactor timecode type detection logic"
101 | msgstr ""
102 |
103 | #: ../../../CHANGELOG.md:38
104 | msgid "重构时码类型检测逻辑"
105 | msgstr ""
106 |
107 | #: ../../../CHANGELOG.md:40
108 | msgid "Refactor error handling during timecode initialization"
109 | msgstr ""
110 |
111 | #: ../../../CHANGELOG.md:42
112 | msgid "重构时码对象初始化时的错误处理"
113 | msgstr ""
114 |
115 | #: ../../../CHANGELOG.md:44
116 | msgid "Modify error messages to enhance readability"
117 | msgstr ""
118 |
119 | #: ../../../CHANGELOG.md:46
120 | msgid "修改报错信息,增强可读性"
121 | msgstr ""
122 |
123 | #: ../../../CHANGELOG.md:48 ../../../CHANGELOG.md:86
124 | msgid "Deprecated"
125 | msgstr ""
126 |
127 | #: ../../../CHANGELOG.md:50
128 | msgid "Completely remove `InstanceMethodDispatch`"
129 | msgstr ""
130 |
131 | #: ../../../CHANGELOG.md:52
132 | msgid "完全移除 `InstanceMethodDispatch`"
133 | msgstr ""
134 |
135 | #: ../../../CHANGELOG.md:56
136 | msgid ""
137 | "Fix millisecond overflow issue when converting certain timecodes from "
138 | "SMPTE to SRT [#19](https://github.com/OwenYou/dftt_timecode/issues/19)"
139 | msgstr ""
140 |
141 | #: ../../../CHANGELOG.md:58
142 | msgid "修复特定时码在SMPTE转SRT时毫秒会溢出的问题"
143 | msgstr ""
144 |
145 | #: ../../../CHANGELOG.md:60
146 | msgid "[0.0.14]"
147 | msgstr ""
148 |
149 | #: ../../../CHANGELOG.md:64
150 | msgid ""
151 | "Fix v0.0.13 import failure caused by missing dftt_timecode.core while "
152 | "packing"
153 | msgstr ""
154 |
155 | #: ../../../CHANGELOG.md:66
156 | msgid "修复v0.0.13打包后不包含core,导致库无法使用的问题"
157 | msgstr ""
158 |
159 | #: ../../../CHANGELOG.md:68
160 | msgid "[0.0.13]"
161 | msgstr ""
162 |
163 | #: ../../../CHANGELOG.md:72
164 | msgid ""
165 | "Add `get_audio_sample_count` method for correctly outputting the count of"
166 | " audio samples at TC timestamps "
167 | "[#9](https://github.com/OwenYou/dftt_timecode/issues/9)"
168 | msgstr ""
169 |
170 | #: ../../../CHANGELOG.md:74
171 | msgid "添加 `get_audio_sample_count` 方法用于正确输出TC时间戳下的音频采样数"
172 | msgstr ""
173 |
174 | #: ../../../CHANGELOG.md:78
175 | msgid "Use f-string for string format output"
176 | msgstr ""
177 |
178 | #: ../../../CHANGELOG.md:80
179 | msgid "使用 `f-string` 处理字符串格式输出"
180 | msgstr ""
181 |
182 | #: ../../../CHANGELOG.md:82
183 | msgid "Refactor timecode output function to reduce code duplication"
184 | msgstr ""
185 |
186 | #: ../../../CHANGELOG.md:84
187 | msgid "重构时间码输出函数,减少代码重复"
188 | msgstr ""
189 |
190 | #: ../../../CHANGELOG.md:88
191 | msgid ""
192 | "Use `functools.singledispatchmethod` instead of "
193 | "`dispatch.InstanceMethodDispatch`"
194 | msgstr ""
195 |
196 | #: ../../../CHANGELOG.md:90
197 | msgid "使用 `functools.singledispatchmethod` 代替 `dispatch.InstanceMethodDispatch`"
198 | msgstr ""
199 |
200 | #: ../../../CHANGELOG.md:92
201 | msgid "[0.0.12]"
202 | msgstr ""
203 |
204 | #: ../../../CHANGELOG.md:96
205 | msgid ""
206 | "Fix DLP pattern error causing DLP ticks in range [50-99] and [150-199] to"
207 | " not be matched. This bug caused errors when using strings like "
208 | "`'00:00:27:183'` to initialize a DLP timecode object"
209 | msgstr ""
210 |
211 | #: ../../../CHANGELOG.md:98
212 | msgid "修复DLP正则表达式错误导致范围在50-99、150-199的DLP tick不能被匹配的问题"
213 | msgstr ""
214 |
215 | #: ../../../CHANGELOG.md:100
216 | msgid "[0.0.11]"
217 | msgstr ""
218 |
219 | #: ../../../CHANGELOG.md:104
220 | msgid "Add `__str__` method to return timecode value for DfttTimecode object"
221 | msgstr ""
222 |
223 | #: ../../../CHANGELOG.md:106
224 | msgid "添加 `__str__` 方法,返回DfttTimecode对象的时间码值"
225 | msgstr ""
226 |
227 | #: ../../../CHANGELOG.md:108
228 | msgid "Add unit tests for DfttTimecode using pytest"
229 | msgstr ""
230 |
231 | #: ../../../CHANGELOG.md:110
232 | msgid "添加DfttTimecode单元测试(使用pytest)"
233 | msgstr ""
234 |
235 | #: ../../../CHANGELOG.md:114
236 | msgid "Add 23.98/23.976 FPS to drop frame detection conditions"
237 | msgstr ""
238 |
239 | #: ../../../CHANGELOG.md:116
240 | msgid "对丢帧的检测条件添加有关23.98/23.976的判定"
241 | msgstr ""
242 |
243 | #: ../../../CHANGELOG.md:118
244 | msgid ""
245 | "`+` and `-` operators perform an OR operation on the strict property of "
246 | "two DfttTimecode objects being added/subtracted"
247 | msgstr ""
248 |
249 | #: ../../../CHANGELOG.md:120
250 | msgid "`+` `-` 运算符对相加的两个DfttTimecode对象的strict属性进行或运算"
251 | msgstr ""
252 |
253 | #: ../../../CHANGELOG.md:122
254 | msgid ""
255 | "Comparison operators (`==`, `>`, `>=`, etc.) now validate that both "
256 | "DfttTimecode objects have the same frame rate before comparison, throwing"
257 | " an exception if frame rates differ"
258 | msgstr ""
259 |
260 | #: ../../../CHANGELOG.md:124
261 | msgid "比较运算符在对两个DfttTimecode对象进行比较时会先判定帧率,若帧率不同则抛出异常"
262 | msgstr ""
263 |
264 | #: ../../../CHANGELOG.md:126
265 | msgid "`print(self)` now outputs a formatted timecode string"
266 | msgstr ""
267 |
268 | #: ../../../CHANGELOG.md:128
269 | msgid "`print(self)` 将会输出基于类型的时间码字符串"
270 | msgstr ""
271 |
272 | #: ../../../CHANGELOG.md:132
273 | msgid ""
274 | "Fix issue in `timecode_output(self, dest_type, output_part)` where "
275 | "`output_part = 3` would incorrectly return minute data"
276 | msgstr ""
277 |
278 | #: ../../../CHANGELOG.md:134
279 | msgid "修复 `timecode_output` 中 `output_part = 3` 时错误返回分钟数据的问题"
280 | msgstr ""
281 |
282 | #: ../../../CHANGELOG.md:136
283 | msgid "[0.0.10]"
284 | msgstr ""
285 |
286 | #: ../../../CHANGELOG.md:140
287 | msgid ""
288 | "Add support for using a DfttTimecode object to initialize a new "
289 | "DfttTimecode object"
290 | msgstr ""
291 |
292 | #: ../../../CHANGELOG.md:142
293 | msgid "使用DfttTimecode对象初始化新DfttTimecode对象"
294 | msgstr ""
295 |
296 | #: ../../../CHANGELOG.md:144
297 | msgid "Add `float()` and `int()` class methods for DfttTimecode class"
298 | msgstr ""
299 |
300 | #: ../../../CHANGELOG.md:146
301 | msgid "添加DfttTimecode类的float和int方法"
302 | msgstr ""
303 |
304 | #: ../../../CHANGELOG.md:148
305 | msgid "Add `precise_timestamp` attribute for DfttTimecode class"
306 | msgstr ""
307 |
308 | #: ../../../CHANGELOG.md:150
309 | msgid "添加DfttTimecode类的precise_timestamp属性"
310 | msgstr ""
311 |
312 | #: ../../../CHANGELOG.md:154
313 | msgid ""
314 | "DfttTimecode operators now raise errors when encountering undefined "
315 | "circumstances or illegal operations"
316 | msgstr ""
317 |
318 | #: ../../../CHANGELOG.md:156
319 | msgid "DfttTimecode运算符在未定义/非法操作时将会报错"
320 | msgstr ""
321 |
322 | #: ../../../CHANGELOG.md:158
323 | msgid "Update comparison rules for DfttTimecode operators"
324 | msgstr ""
325 |
326 | #: ../../../CHANGELOG.md:160
327 | msgid "修改DfttTimecode运算符的大小比较规则"
328 | msgstr ""
329 |
330 | #: ../../../CHANGELOG.md:162
331 | msgid ""
332 | "When creating a timecode object using SMPTE NDF format string, if "
333 | "`drop_frame` is forced to `True`, the resulting object will be SMPTE DF "
334 | "format timecode"
335 | msgstr ""
336 |
337 | #: ../../../CHANGELOG.md:164
338 | msgid "使用SMPTE NDF格式字符串新建时码类对象时,若强制drop_frame为True,则新建得到的对象为SMPTE DF格式时码"
339 | msgstr ""
340 |
341 | #: ../../../CHANGELOG.md:166
342 | msgid "[0.0.9]"
343 | msgstr ""
344 |
345 | #: ../../../CHANGELOG.md:168
346 | msgid "Initial public release."
347 | msgstr ""
348 |
349 |
--------------------------------------------------------------------------------
/test/test_logging_config.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for logging configuration module.
3 |
4 | Tests the automatic log level detection based on git branch and
5 | environment variable configuration.
6 | """
7 |
8 | import logging
9 | import subprocess
10 | from unittest.mock import patch
11 |
12 | from dftt_timecode.logging_config import (
13 | _determine_log_level,
14 | _get_git_branch,
15 | configure_logging,
16 | get_logger,
17 | )
18 |
19 |
20 | class TestGitBranchDetection:
21 | """Test git branch detection functionality."""
22 |
23 | def test_get_git_branch_in_repo(self):
24 | """Test that _get_git_branch returns a branch name when in a git repo."""
25 | branch = _get_git_branch()
26 | # Should return a string (branch name) or None if not in repo
27 | assert branch is None or isinstance(branch, str)
28 |
29 | @patch('subprocess.run')
30 | def test_get_git_branch_success(self, mock_run):
31 | """Test successful git branch detection."""
32 | mock_run.return_value.returncode = 0
33 | mock_run.return_value.stdout = 'main\n'
34 |
35 | branch = _get_git_branch()
36 | assert branch == 'main'
37 |
38 | @patch('subprocess.run')
39 | def test_get_git_branch_not_in_repo(self, mock_run):
40 | """Test behavior when not in a git repository."""
41 | mock_run.return_value.returncode = 128
42 | mock_run.return_value.stdout = ''
43 |
44 | branch = _get_git_branch()
45 | assert branch is None
46 |
47 | @patch('subprocess.run')
48 | def test_get_git_branch_timeout(self, mock_run):
49 | """Test behavior when git command times out."""
50 | mock_run.side_effect = subprocess.TimeoutExpired('git', 2)
51 |
52 | branch = _get_git_branch()
53 | assert branch is None
54 |
55 | @patch('subprocess.run')
56 | def test_get_git_branch_file_not_found(self, mock_run):
57 | """Test behavior when git is not installed."""
58 | mock_run.side_effect = FileNotFoundError()
59 |
60 | branch = _get_git_branch()
61 | assert branch is None
62 |
63 |
64 | class TestLogLevelDetermination:
65 | """Test log level determination logic."""
66 |
67 | def test_determine_log_level_with_env_debug(self, monkeypatch):
68 | """Test that DFTT_LOG_LEVEL env var sets DEBUG level."""
69 | monkeypatch.setenv('DFTT_LOG_LEVEL', 'DEBUG')
70 | level = _determine_log_level()
71 | assert level == logging.DEBUG
72 |
73 | def test_determine_log_level_with_env_info(self, monkeypatch):
74 | """Test that DFTT_LOG_LEVEL env var sets INFO level."""
75 | monkeypatch.setenv('DFTT_LOG_LEVEL', 'INFO')
76 | level = _determine_log_level()
77 | assert level == logging.INFO
78 |
79 | def test_determine_log_level_with_env_warning(self, monkeypatch):
80 | """Test that DFTT_LOG_LEVEL env var sets WARNING level."""
81 | monkeypatch.setenv('DFTT_LOG_LEVEL', 'WARNING')
82 | level = _determine_log_level()
83 | assert level == logging.WARNING
84 |
85 | def test_determine_log_level_with_env_error(self, monkeypatch):
86 | """Test that DFTT_LOG_LEVEL env var sets ERROR level."""
87 | monkeypatch.setenv('DFTT_LOG_LEVEL', 'ERROR')
88 | level = _determine_log_level()
89 | assert level == logging.ERROR
90 |
91 | def test_determine_log_level_with_env_critical(self, monkeypatch):
92 | """Test that DFTT_LOG_LEVEL env var sets CRITICAL level."""
93 | monkeypatch.setenv('DFTT_LOG_LEVEL', 'CRITICAL')
94 | level = _determine_log_level()
95 | assert level == logging.CRITICAL
96 |
97 | @patch('dftt_timecode.logging_config._get_git_branch')
98 | def test_determine_log_level_with_invalid_env(self, mock_get_branch, monkeypatch):
99 | """Test that invalid DFTT_LOG_LEVEL env var is ignored."""
100 | monkeypatch.setenv('DFTT_LOG_LEVEL', 'INVALID')
101 | # Mock git to return a dev branch for predictable testing
102 | mock_get_branch.return_value = 'dev'
103 | level = _determine_log_level()
104 | # Should fall back to branch-based detection
105 | assert level == logging.DEBUG
106 |
107 | @patch('dftt_timecode.logging_config._get_git_branch')
108 | def test_determine_log_level_main_branch(self, mock_get_branch, monkeypatch):
109 | """Test that main branch gets INFO level."""
110 | monkeypatch.delenv('DFTT_LOG_LEVEL', raising=False)
111 | mock_get_branch.return_value = 'main'
112 |
113 | level = _determine_log_level()
114 | assert level == logging.INFO
115 |
116 | @patch('dftt_timecode.logging_config._get_git_branch')
117 | def test_determine_log_level_dev_branch(self, mock_get_branch, monkeypatch):
118 | """Test that dev branch gets DEBUG level."""
119 | monkeypatch.delenv('DFTT_LOG_LEVEL', raising=False)
120 | mock_get_branch.return_value = 'dev'
121 |
122 | level = _determine_log_level()
123 | assert level == logging.DEBUG
124 |
125 | @patch('dftt_timecode.logging_config._get_git_branch')
126 | def test_determine_log_level_feature_branch(self, mock_get_branch, monkeypatch):
127 | """Test that feature branches get DEBUG level."""
128 | monkeypatch.delenv('DFTT_LOG_LEVEL', raising=False)
129 | mock_get_branch.return_value = 'feature/new-feature'
130 |
131 | level = _determine_log_level()
132 | assert level == logging.DEBUG
133 |
134 | @patch('dftt_timecode.logging_config._get_git_branch')
135 | def test_determine_log_level_no_git(self, mock_get_branch, monkeypatch):
136 | """Test that INFO is used when git info unavailable (built packages)."""
137 | monkeypatch.delenv('DFTT_LOG_LEVEL', raising=False)
138 | mock_get_branch.return_value = None
139 |
140 | level = _determine_log_level()
141 | assert level == logging.INFO
142 |
143 | def test_env_var_overrides_branch(self, monkeypatch):
144 | """Test that environment variable takes priority over branch detection."""
145 | monkeypatch.setenv('DFTT_LOG_LEVEL', 'ERROR')
146 | # Even if we're on main branch, env var should win
147 | level = _determine_log_level()
148 | assert level == logging.ERROR
149 |
150 | @patch('dftt_timecode.logging_config._get_git_branch')
151 | def test_installed_package_defaults_to_info(self, mock_get_branch, monkeypatch):
152 | """Test that installed packages (no git) default to INFO level."""
153 | monkeypatch.delenv('DFTT_LOG_LEVEL', raising=False)
154 | mock_get_branch.return_value = None
155 |
156 | level = _determine_log_level()
157 | # Built/installed packages should use INFO, not DEBUG
158 | assert level == logging.INFO
159 |
160 |
161 | class TestGetLogger:
162 | """Test logger creation and configuration."""
163 |
164 | def test_get_logger_returns_logger(self):
165 | """Test that get_logger returns a Logger instance."""
166 | logger = get_logger('test_module')
167 | assert isinstance(logger, logging.Logger)
168 |
169 | def test_get_logger_has_handler(self):
170 | """Test that logger has at least one handler configured."""
171 | logger = get_logger('test_module_2')
172 | assert len(logger.handlers) > 0
173 |
174 | def test_get_logger_has_formatter(self):
175 | """Test that logger's handler has a formatter configured."""
176 | logger = get_logger('test_module_3')
177 | handler = logger.handlers[0]
178 | assert handler.formatter is not None
179 |
180 | def test_get_logger_no_propagate(self):
181 | """Test that logger doesn't propagate to root logger."""
182 | logger = get_logger('test_module_4')
183 | assert logger.propagate is False
184 |
185 | def test_get_logger_idempotent(self):
186 | """Test that calling get_logger multiple times doesn't add duplicate handlers."""
187 | logger1 = get_logger('test_module_5')
188 | handler_count_1 = len(logger1.handlers)
189 |
190 | logger2 = get_logger('test_module_5')
191 | handler_count_2 = len(logger2.handlers)
192 |
193 | assert logger1 is logger2
194 | assert handler_count_1 == handler_count_2
195 |
196 | @patch('dftt_timecode.logging_config._get_git_branch')
197 | def test_get_logger_level_main_branch(self, mock_get_branch, monkeypatch):
198 | """Test that logger has INFO level on main branch."""
199 | monkeypatch.delenv('DFTT_LOG_LEVEL', raising=False)
200 | mock_get_branch.return_value = 'main'
201 |
202 | logger = get_logger('test_module_main')
203 | assert logger.level == logging.INFO
204 |
205 | @patch('dftt_timecode.logging_config._get_git_branch')
206 | def test_get_logger_level_dev_branch(self, mock_get_branch, monkeypatch):
207 | """Test that logger has DEBUG level on dev branch."""
208 | monkeypatch.delenv('DFTT_LOG_LEVEL', raising=False)
209 | mock_get_branch.return_value = 'dev'
210 |
211 | logger = get_logger('test_module_dev')
212 | assert logger.level == logging.DEBUG
213 |
214 |
215 | class TestConfigureLogging:
216 | """Test manual logging configuration."""
217 |
218 | def test_configure_logging_with_level(self):
219 | """Test that configure_logging sets specified level."""
220 | # Create a test logger first
221 | logger = get_logger('dftt_timecode.test_config')
222 |
223 | # Configure to WARNING
224 | configure_logging(logging.WARNING)
225 |
226 | # Check that the level was updated
227 | assert logger.level == logging.WARNING
228 |
229 | @patch('dftt_timecode.logging_config._get_git_branch')
230 | def test_configure_logging_without_level(self, mock_get_branch):
231 | """Test that configure_logging uses automatic detection when level not specified."""
232 | # Create a test logger first
233 | logger = get_logger('dftt_timecode.test_config_2')
234 |
235 | # Mock git to return main branch
236 | mock_get_branch.return_value = 'main'
237 |
238 | # Configure without specifying level
239 | configure_logging()
240 |
241 | # Should use branch-based detection (INFO for main)
242 | assert logger.level == logging.INFO
243 |
244 | def test_configure_logging_updates_handlers(self):
245 | """Test that configure_logging updates handler levels."""
246 | # Create a test logger first
247 | logger = get_logger('dftt_timecode.test_config_3')
248 |
249 | # Configure to ERROR
250 | configure_logging(logging.ERROR)
251 |
252 | # Check that handlers were updated
253 | for handler in logger.handlers:
254 | assert handler.level == logging.ERROR
255 |
256 |
257 | class TestIntegration:
258 | """Integration tests for logging system."""
259 |
260 | def test_dftt_timecode_uses_logging_config(self):
261 | """Test that DfttTimecode module uses the new logging config."""
262 | from dftt_timecode.core.dftt_timecode import logger
263 |
264 | # Should be a proper Logger instance
265 | assert isinstance(logger, logging.Logger)
266 |
267 | # Should have handlers configured
268 | assert len(logger.handlers) > 0
269 |
270 | def test_logging_exports_from_main_package(self):
271 | """Test that logging functions are exported from main package."""
272 | from dftt_timecode import configure_logging as conf
273 | from dftt_timecode import get_logger as gl
274 |
275 | assert callable(conf)
276 | assert callable(gl)
277 |
278 | def test_real_world_usage(self):
279 | """Test a real-world usage scenario."""
280 | # Import and create a timecode
281 | from dftt_timecode import DfttTimecode
282 |
283 | # This should not raise any errors
284 | tc = DfttTimecode('01:00:00:00', fps=24)
285 | assert tc is not None
286 |
--------------------------------------------------------------------------------
/docs/contributing.rst:
--------------------------------------------------------------------------------
1 | Contributing
2 | ============
3 |
4 | We welcome contributions to dftt_timecode! This guide will help you get started.
5 |
6 | Getting Started
7 | ---------------
8 |
9 | 1. Fork the repository on GitHub
10 | 2. Clone your fork locally:
11 |
12 | .. code-block:: bash
13 |
14 | git clone https://github.com/YOUR_USERNAME/dftt_timecode.git
15 | cd dftt_timecode
16 |
17 | Development Workflow
18 | --------------------
19 |
20 | Setting Up Your Environment
21 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~
22 |
23 | Create a virtual environment and install dependencies:
24 |
25 | .. code-block:: bash
26 |
27 | uv sync
28 |
29 | This command will:
30 |
31 | - Create a virtual environment (if not already present)
32 | - Install all dependencies from ``pyproject.toml``
33 | - Install the package in editable mode
34 | - Generate/update ``uv.lock`` for reproducible builds
35 |
36 | Running Tests
37 | ~~~~~~~~~~~~~
38 |
39 | Run all tests:
40 |
41 | .. code-block:: bash
42 |
43 | uv run pytest
44 |
45 | Run tests with verbose output:
46 |
47 | .. code-block:: bash
48 |
49 | uv run pytest -v -s
50 |
51 | Run specific test file:
52 |
53 | .. code-block:: bash
54 |
55 | uv run pytest test/test_dftt_timecode.py
56 |
57 | Code Style
58 | ----------
59 |
60 | - Follow PEP 8 guidelines
61 | - Use meaningful variable and function names
62 | - Add docstrings to all public functions and classes
63 | - Keep functions focused and concise
64 |
65 | Writing Tests
66 | -------------
67 |
68 | All new features and bug fixes should include tests:
69 |
70 | .. code-block:: python
71 |
72 | import pytest
73 | from dftt_timecode import DfttTimecode
74 |
75 | def test_new_feature():
76 | tc = DfttTimecode('01:00:00:00', 'auto', fps=24)
77 | # Test your feature
78 | assert tc.some_new_method() == expected_result
79 |
80 | Documentation
81 | -------------
82 |
83 | Update documentation when adding new features:
84 |
85 | 1. Add docstrings to your code
86 | 2. Update relevant .rst files in the docs/ directory
87 | 3. Build documentation locally to verify:
88 |
89 | .. code-block:: bash
90 |
91 | cd docs
92 | uv run make html
93 |
94 | Contributing Translations
95 | ~~~~~~~~~~~~~~~~~~~~~~~~~
96 |
97 | We welcome contributions to documentation translations! Currently we support:
98 |
99 | - **English** (primary language)
100 | - **中文 (Simplified Chinese)**
101 |
102 | How to Contribute Translations
103 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
104 |
105 | **Adding new translations to existing languages:**
106 |
107 | 1. Navigate to the translation files:
108 |
109 | .. code-block:: bash
110 |
111 | cd docs/locale/zh_CN/LC_MESSAGES/
112 |
113 | 2. Edit the ``.po`` files to add or improve translations:
114 |
115 | .. code-block:: po
116 |
117 | #: ../../quickstart.rst:2
118 | msgid "Quick Start"
119 | msgstr "快速开始"
120 |
121 | 3. Build and preview your translations:
122 |
123 | .. code-block:: bash
124 |
125 | cd docs
126 | uv run make html-zh # Build Chinese only
127 | uv run make html-all # Build all languages
128 |
129 | 4. Preview locally:
130 |
131 | .. code-block:: bash
132 |
133 | cd docs/_build/html
134 | python -m http.server 8000
135 | # Visit http://localhost:8000/zh_CN/
136 |
137 | **Adding a new language:**
138 |
139 | 1. Generate translation files for your language:
140 |
141 | .. code-block:: bash
142 |
143 | cd docs
144 | uv run sphinx-intl update -p _build/gettext -l
145 | # e.g., for Japanese: -l ja
146 |
147 | 2. Update ``docs/Makefile`` to include the new language in the ``LANGUAGES`` variable
148 |
149 | 3. Update ``docs/_static/switcher.json`` to add your language option
150 |
151 | 4. Update the language switcher template in ``docs/_templates/language-switcher.html``
152 |
153 | 5. Translate the ``.po`` files in ``locale//LC_MESSAGES/``
154 |
155 | 6. Build and test your translation
156 |
157 | **Translation Guidelines:**
158 |
159 | - **User documentation** (installation, quickstart, user guide): Translate everything
160 | - **API documentation**: Translate page titles and main descriptions, but keep technical details (class names, method names, parameters) in English
161 | - **Changelog**: Translate section headers, but keep technical change entries in English
162 | - **Code examples**: Keep code and variable names in English
163 | - **Technical terms**: Use consistent translations (see the translation guide in ``docs/I18N_README.md``)
164 |
165 | **Updating translations when source changes:**
166 |
167 | When documentation source files are updated, translations need to be updated:
168 |
169 | .. code-block:: bash
170 |
171 | cd docs
172 | uv run make gettext # Generate new translation templates
173 | uv run make update-po # Update .po files with new strings
174 | # Edit .po files to translate new or updated strings
175 | uv run make html-all # Rebuild documentation
176 |
177 | **Translation System Overview**
178 |
179 | The project uses Sphinx with ``sphinx-intl`` for internationalization. The system uses gettext ``.po`` (Portable Object) files for translations, which is the industry standard for software localization.
180 |
181 | **File Structure:**
182 |
183 | ::
184 |
185 | docs/
186 | ├── locale/ # Translation files directory
187 | │ └── zh_CN/
188 | │ └── LC_MESSAGES/
189 | │ ├── index.po # Translations for index.rst
190 | │ ├── quickstart.po
191 | │ ├── user_guide.po
192 | │ └── api/
193 | │ ├── dftt_timecode.po
194 | │ ├── dftt_timerange.po
195 | │ └── error.po
196 | ├── _build/
197 | │ └── gettext/ # Generated .pot template files (don't commit)
198 | └── _templates/
199 | └── language-switcher.html # Language switcher dropdown widget
200 |
201 | **Detailed Translation Workflow:**
202 |
203 | 1. **Generate translation templates** (when source docs change):
204 |
205 | .. code-block:: bash
206 |
207 | cd docs
208 | uv run make gettext
209 |
210 | This creates ``.pot`` files in ``_build/gettext/`` containing all translatable strings.
211 |
212 | 2. **Update translation files**:
213 |
214 | .. code-block:: bash
215 |
216 | cd docs
217 | uv run make update-po
218 |
219 | This updates ``.po`` files in ``locale/zh_CN/LC_MESSAGES/`` with new strings while preserving existing translations.
220 |
221 | 3. **Translate the strings**:
222 |
223 | Open ``.po`` files and add translations:
224 |
225 | .. code-block:: po
226 |
227 | #: ../../index.rst:70
228 | msgid "User Guide"
229 | msgstr "用户指南"
230 |
231 | #: ../../index.rst:78
232 | msgid "API Reference"
233 | msgstr "API 参考"
234 |
235 | **Translation Tips:**
236 |
237 | - Each ``msgid`` contains the original English text
238 | - Add your translation in the corresponding ``msgstr`` field
239 | - Preserve formatting codes like ``{0}``, ``%s``, etc.
240 | - Keep technical terms (class/function names) untranslated
241 | - Use tools like `Poedit `_ for easier editing
242 |
243 | 4. **Build and test**:
244 |
245 | .. code-block:: bash
246 |
247 | cd docs
248 | uv run make html-zh # Build Chinese only
249 | uv run make html-all # Build all languages
250 |
251 | Preview the result:
252 |
253 | .. code-block:: bash
254 |
255 | cd docs/_build/html
256 | python -m http.server 8000
257 | # Visit http://localhost:8000/zh_CN/
258 |
259 | 5. **Commit your changes**:
260 |
261 | .. code-block:: bash
262 |
263 | git add locale/
264 | git commit -m "Update Chinese translation for user guide"
265 |
266 | **Important Makefile Commands:**
267 |
268 | - ``make gettext``: Generate ``.pot`` template files from source ``.rst`` files
269 | - ``make update-po``: Update ``.po`` files from ``.pot`` templates
270 | - ``make html``: Build English documentation only
271 | - ``make html-zh``: Build Chinese documentation only
272 | - ``make html-all``: Build all language versions
273 |
274 | **Common Issues and Solutions:**
275 |
276 | **Translations not showing:**
277 |
278 | 1. Ensure ``.po`` files have non-empty ``msgstr`` values
279 | 2. Rebuild with ``uv run make html-all``
280 | 3. Clear browser cache or use incognito mode
281 |
282 | **New strings not appearing in .po files:**
283 |
284 | 1. Run ``uv run make gettext`` to regenerate ``.pot`` files
285 | 2. Run ``uv run make update-po`` to update ``.po`` files
286 | 3. Check that your source ``.rst`` files are included in the build
287 |
288 | **Language switcher not working:**
289 |
290 | 1. Verify ``_templates/language-switcher.html`` exists
291 | 2. Ensure target language HTML was built in correct subdirectory (``_build/html/zh_CN/``)
292 |
293 | **Best Practices:**
294 |
295 | - **Commit ``.po`` files**: Always commit updated ``.po`` files to version control
296 | - **Don't commit ``.pot`` files**: These are generated artifacts in ``_build/gettext/``
297 | - **Incremental translation**: It's okay to commit partially translated ``.po`` files; untranslated strings display in English
298 | - **Review before push**: Build and preview locally before pushing translations
299 | - **Consistent terminology**: Use consistent translations for technical terms across all pages
300 | - **Keep source in sync**: Run ``make update-po`` regularly to sync with source changes
301 |
302 | **Automated Deployment:**
303 |
304 | Documentation is automatically built and deployed via GitHub Actions when pushed to ``main``:
305 |
306 | - Workflow: ``.github/workflows/docs.yml``
307 | - Build command: ``uv run make html-all``
308 | - Deployment: GitHub Pages at https://owenyou.github.io/dftt_timecode/
309 |
310 | When you push translated ``.po`` files to the ``main`` branch (via ``dev`` merge), the multilingual documentation is automatically rebuilt and deployed.
311 |
312 | **Additional Resources:**
313 |
314 | - `Sphinx Internationalization `_
315 | - `sphinx-intl Documentation `_
316 | - `GNU gettext Documentation `_
317 |
318 | Submitting Changes
319 | ------------------
320 |
321 | 1. Create a new branch for your changes:
322 |
323 | .. code-block:: bash
324 |
325 | git checkout -b feature/my-new-feature
326 |
327 | 2. Make your changes and commit:
328 |
329 | .. code-block:: bash
330 |
331 | git add .
332 | git commit -m "Add new feature: description"
333 |
334 | 3. Push to your fork:
335 |
336 | .. code-block:: bash
337 |
338 | git push origin feature/my-new-feature
339 |
340 | 4. Open a Pull Request on GitHub
341 |
342 | Pull Request Guidelines
343 | -----------------------
344 |
345 | - Provide a clear description of the changes
346 | - Reference any related issues
347 | - Ensure all tests pass
348 | - Update documentation as needed
349 | - Keep changes focused and atomic
350 |
351 | Reporting Bugs
352 | --------------
353 |
354 | When reporting bugs, please include:
355 |
356 | - Python version
357 | - dftt_timecode version
358 | - Minimal code example that reproduces the issue
359 | - Expected vs actual behavior
360 | - Any error messages or stack traces
361 |
362 | Feature Requests
363 | ----------------
364 |
365 | Feature requests are welcome! Please provide:
366 |
367 | - Clear description of the feature
368 | - Use cases and examples
369 | - Why this would be valuable to other users
370 |
371 | Code of Conduct
372 | ---------------
373 |
374 | - Be respectful and inclusive
375 | - Focus on constructive feedback
376 | - Help create a welcoming environment for all contributors
377 |
378 | License
379 | -------
380 |
381 | By contributing, you agree that your contributions will be licensed under the
382 | GNU Lesser General Public License v2 (LGPLv2).
383 |
--------------------------------------------------------------------------------
/docs/locale/zh_CN/LC_MESSAGES/contributing.po:
--------------------------------------------------------------------------------
1 | # DFTT Timecode Documentation Chinese Translation
2 | # Copyright (C) 2025, You Ziyuan
3 | # This file is distributed under the same license as the DFTT Timecode
4 | # package.
5 | # You Ziyuan , 2025.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: DFTT Timecode 0.0.15a2\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2025-10-21 13:18+0800\n"
12 | "PO-Revision-Date: 2025-10-21 12:00+0800\n"
13 | "Last-Translator: You Ziyuan \n"
14 | "Language: zh_CN\n"
15 | "Language-Team: zh_CN \n"
16 | "Plural-Forms: nplurals=1; plural=0;\n"
17 | "MIME-Version: 1.0\n"
18 | "Content-Type: text/plain; charset=utf-8\n"
19 | "Content-Transfer-Encoding: 8bit\n"
20 | "Generated-By: Babel 2.17.0\n"
21 |
22 | #: ../../contributing.rst:2
23 | msgid "Contributing"
24 | msgstr "贡献指南"
25 |
26 | #: ../../contributing.rst:4
27 | msgid ""
28 | "We welcome contributions to dftt_timecode! This guide will help you get "
29 | "started."
30 | msgstr "我们欢迎对 dftt_timecode 的贡献!本指南将帮助您开始贡献。"
31 |
32 | #: ../../contributing.rst:7
33 | msgid "Getting Started"
34 | msgstr "开始"
35 |
36 | #: ../../contributing.rst:9
37 | msgid "Fork the repository on GitHub"
38 | msgstr "在 GitHub 上 Fork 仓库"
39 |
40 | #: ../../contributing.rst:10
41 | msgid "Clone your fork locally:"
42 | msgstr "在本地克隆您的 Fork:"
43 |
44 | #: ../../contributing.rst:18
45 | msgid "Development Workflow"
46 | msgstr "开发工作流"
47 |
48 | #: ../../contributing.rst:21
49 | msgid "Setting Up Your Environment"
50 | msgstr "设置开发环境"
51 |
52 | #: ../../contributing.rst:23
53 | msgid "Create a virtual environment and install dependencies:"
54 | msgstr "创建虚拟环境并安装依赖:"
55 |
56 | #: ../../contributing.rst:29
57 | msgid "This command will:"
58 | msgstr "此命令将:"
59 |
60 | #: ../../contributing.rst:31
61 | msgid "Create a virtual environment (if not already present)"
62 | msgstr "创建虚拟环境(如果尚不存在)"
63 |
64 | #: ../../contributing.rst:32
65 | msgid "Install all dependencies from ``pyproject.toml``"
66 | msgstr "从 ``pyproject.toml`` 安装所有依赖"
67 |
68 | #: ../../contributing.rst:33
69 | msgid "Install the package in editable mode"
70 | msgstr "以可编辑模式安装包"
71 |
72 | #: ../../contributing.rst:34
73 | msgid "Generate/update ``uv.lock`` for reproducible builds"
74 | msgstr "生成/更新 ``uv.lock`` 以实现可重现构建"
75 |
76 | #: ../../contributing.rst:37
77 | msgid "Running Tests"
78 | msgstr "运行测试"
79 |
80 | #: ../../contributing.rst:39
81 | msgid "Run all tests:"
82 | msgstr "运行所有测试:"
83 |
84 | #: ../../contributing.rst:45
85 | msgid "Run tests with verbose output:"
86 | msgstr "以详细输出运行测试:"
87 |
88 | #: ../../contributing.rst:51
89 | msgid "Run specific test file:"
90 | msgstr "运行特定测试文件:"
91 |
92 | #: ../../contributing.rst:58
93 | msgid "Code Style"
94 | msgstr "代码风格"
95 |
96 | #: ../../contributing.rst:60
97 | msgid "Follow PEP 8 guidelines"
98 | msgstr "遵循 PEP 8 规范"
99 |
100 | #: ../../contributing.rst:61
101 | msgid "Use meaningful variable and function names"
102 | msgstr "使用有意义的变量和函数名"
103 |
104 | #: ../../contributing.rst:62
105 | msgid "Add docstrings to all public functions and classes"
106 | msgstr "为所有公共函数和类添加文档字符串"
107 |
108 | #: ../../contributing.rst:63
109 | msgid "Keep functions focused and concise"
110 | msgstr "保持函数专注和简洁"
111 |
112 | #: ../../contributing.rst:66
113 | msgid "Writing Tests"
114 | msgstr "编写测试"
115 |
116 | #: ../../contributing.rst:68
117 | msgid "All new features and bug fixes should include tests:"
118 | msgstr "所有新功能和错误修复都应包含测试:"
119 |
120 | #: ../../contributing.rst:81
121 | msgid "Documentation"
122 | msgstr "文档"
123 |
124 | #: ../../contributing.rst:83
125 | msgid "Update documentation when adding new features:"
126 | msgstr "添加新功能时更新文档:"
127 |
128 | #: ../../contributing.rst:85
129 | msgid "Add docstrings to your code"
130 | msgstr "为代码添加文档字符串"
131 |
132 | #: ../../contributing.rst:86
133 | msgid "Update relevant .rst files in the docs/ directory"
134 | msgstr "更新 docs/ 目录中的相关 .rst 文件"
135 |
136 | #: ../../contributing.rst:87
137 | msgid "Build documentation locally to verify:"
138 | msgstr "本地构建文档以验证:"
139 |
140 | #: ../../contributing.rst:95
141 | msgid "Contributing Translations"
142 | msgstr "贡献翻译"
143 |
144 | #: ../../contributing.rst:97
145 | msgid ""
146 | "We welcome contributions to documentation translations! Currently we "
147 | "support:"
148 | msgstr "我们欢迎为文档翻译做出贡献!目前我们支持:"
149 |
150 | #: ../../contributing.rst:99
151 | msgid "**English** (primary language)"
152 | msgstr "英文(主要语言)"
153 |
154 | #: ../../contributing.rst:100
155 | msgid "**中文 (Simplified Chinese)**"
156 | msgstr "中文(简体)"
157 |
158 | #: ../../contributing.rst:103
159 | msgid "How to Contribute Translations"
160 | msgstr "如何贡献翻译"
161 |
162 | #: ../../contributing.rst:105
163 | msgid "**Adding new translations to existing languages:**"
164 | msgstr "**向现有语言添加新翻译:**"
165 |
166 | #: ../../contributing.rst:107
167 | msgid "Navigate to the translation files:"
168 | msgstr "进入翻译文件目录:"
169 |
170 | #: ../../contributing.rst:113
171 | msgid "Edit the ``.po`` files to add or improve translations:"
172 | msgstr "编辑 ``.po`` 文件以添加或改进翻译:"
173 |
174 | #: ../../contributing.rst:121
175 | msgid "Build and preview your translations:"
176 | msgstr "构建并预览您的翻译:"
177 |
178 | #: ../../contributing.rst:129
179 | msgid "Preview locally:"
180 | msgstr "本地预览:"
181 |
182 | #: ../../contributing.rst:137
183 | msgid "**Adding a new language:**"
184 | msgstr "**添加新语言:**"
185 |
186 | #: ../../contributing.rst:139
187 | msgid "Generate translation files for your language:"
188 | msgstr "为您的语言生成翻译文件:"
189 |
190 | #: ../../contributing.rst:147
191 | msgid ""
192 | "Update ``docs/Makefile`` to include the new language in the ``LANGUAGES``"
193 | " variable"
194 | msgstr "更新 ``docs/Makefile``,在 ``LANGUAGES`` 变量中添加新语言"
195 |
196 | #: ../../contributing.rst:149
197 | msgid "Update ``docs/_static/switcher.json`` to add your language option"
198 | msgstr "更新 ``docs/_static/switcher.json`` 以添加您的语言选项"
199 |
200 | #: ../../contributing.rst:151
201 | msgid ""
202 | "Update the language switcher template in ``docs/_templates/language-"
203 | "switcher.html``"
204 | msgstr "更新 ``docs/_templates/language-switcher.html`` 中的语言切换器模板"
205 |
206 | #: ../../contributing.rst:153
207 | msgid "Translate the ``.po`` files in ``locale//LC_MESSAGES/``"
208 | msgstr "翻译 ``locale//LC_MESSAGES/`` 中的 ``.po`` 文件"
209 |
210 | #: ../../contributing.rst:155
211 | msgid "Build and test your translation"
212 | msgstr "构建并测试您的翻译"
213 |
214 | #: ../../contributing.rst:157
215 | msgid "**Translation Guidelines:**"
216 | msgstr "**翻译指南:**"
217 |
218 | #: ../../contributing.rst:159
219 | msgid ""
220 | "**User documentation** (installation, quickstart, user guide): Translate "
221 | "everything"
222 | msgstr "**用户文档** (安装、快速开始、用户指南):翻译所有内容"
223 |
224 | #: ../../contributing.rst:160
225 | msgid ""
226 | "**API documentation**: Translate page titles and main descriptions, but "
227 | "keep technical details (class names, method names, parameters) in English"
228 | msgstr "**API 文档**:翻译页面标题和主要描述,但保持技术细节(类名、方法名、参数)为英文"
229 |
230 | #: ../../contributing.rst:161
231 | msgid ""
232 | "**Changelog**: Translate section headers, but keep technical change "
233 | "entries in English"
234 | msgstr "**更新日志**:翻译章节标题,但保持技术变更条目为英文"
235 |
236 | #: ../../contributing.rst:162
237 | msgid "**Code examples**: Keep code and variable names in English"
238 | msgstr "**代码示例**:保持代码和变量名为英文"
239 |
240 | #: ../../contributing.rst:163
241 | msgid ""
242 | "**Technical terms**: Use consistent translations (see the translation "
243 | "guide in ``docs/I18N_README.md``)"
244 | msgstr "**技术术语**:使用一致的翻译(参见 ``docs/I18N_README.md`` 中的翻译指南)"
245 |
246 | #: ../../contributing.rst:165
247 | msgid "**Updating translations when source changes:**"
248 | msgstr "**当源文件更改时更新翻译:**"
249 |
250 | #: ../../contributing.rst:167
251 | msgid ""
252 | "When documentation source files are updated, translations need to be "
253 | "updated:"
254 | msgstr "当文档源文件更新时,需要更新翻译:"
255 |
256 | #: ../../contributing.rst:177
257 | msgid "**Translation System Overview**"
258 | msgstr "**翻译系统概述**"
259 |
260 | #: ../../contributing.rst:179
261 | msgid ""
262 | "The project uses Sphinx with ``sphinx-intl`` for internationalization. "
263 | "The system uses gettext ``.po`` (Portable Object) files for translations,"
264 | " which is the industry standard for software localization."
265 | msgstr ""
266 | "本项目使用 Sphinx 和 ``sphinx-intl`` 进行国际化。"
267 | "系统使用 gettext ``.po`` (Portable Object)文件进行翻译,"
268 | "这是软件本地化的行业标准。"
269 |
270 | #: ../../contributing.rst:181
271 | msgid "**File Structure:**"
272 | msgstr "**文件结构:**"
273 |
274 | #: ../../contributing.rst:201
275 | msgid "**Detailed Translation Workflow:**"
276 | msgstr "**详细翻译工作流:**"
277 |
278 | #: ../../contributing.rst:203
279 | msgid "**Generate translation templates** (when source docs change):"
280 | msgstr "**生成翻译模板** (当源文档更改时):"
281 |
282 | #: ../../contributing.rst:210
283 | msgid ""
284 | "This creates ``.pot`` files in ``_build/gettext/`` containing all "
285 | "translatable strings."
286 | msgstr ""
287 | "这将在 ``_build/gettext/`` 中创建 ``.pot`` 文件,包含所有可翻译的字符串。"
288 |
289 | #: ../../contributing.rst:212
290 | msgid "**Update translation files**:"
291 | msgstr "**更新翻译文件:**"
292 |
293 | #: ../../contributing.rst:219
294 | msgid ""
295 | "This updates ``.po`` files in ``locale/zh_CN/LC_MESSAGES/`` with new "
296 | "strings while preserving existing translations."
297 | msgstr ""
298 | "这将使用新字符串更新 ``locale/zh_CN/LC_MESSAGES/`` 中的 ``.po`` 文件,"
299 | "同时保留现有翻译。"
300 |
301 | #: ../../contributing.rst:221
302 | msgid "**Translate the strings**:"
303 | msgstr "**翻译字符串:**"
304 |
305 | #: ../../contributing.rst:223
306 | msgid "Open ``.po`` files and add translations:"
307 | msgstr "打开 ``.po`` 文件并添加翻译:"
308 |
309 | #: ../../contributing.rst:235
310 | msgid "**Translation Tips:**"
311 | msgstr "**翻译提示:**"
312 |
313 | #: ../../contributing.rst:237
314 | msgid "Each ``msgid`` contains the original English text"
315 | msgstr "每个 ``msgid`` 包含原始英文文本"
316 |
317 | #: ../../contributing.rst:238
318 | msgid "Add your translation in the corresponding ``msgstr`` field"
319 | msgstr "在相应的 ``msgstr`` 字段中添加您的翻译"
320 |
321 | #: ../../contributing.rst:239
322 | #, python-brace-format, python-format
323 | msgid "Preserve formatting codes like ``{0}``, ``%s``, etc."
324 | msgstr "保留格式化代码,如 ``{0}``、``%s`` 等"
325 |
326 | #: ../../contributing.rst:240
327 | msgid "Keep technical terms (class/function names) untranslated"
328 | msgstr "保持技术术语(类名/函数名)不翻译"
329 |
330 | #: ../../contributing.rst:241
331 | msgid "Use tools like `Poedit `_ for easier editing"
332 | msgstr "使用 `Poedit `_ 等工具进行更轻松的编辑"
333 |
334 | #: ../../contributing.rst:243
335 | msgid "**Build and test**:"
336 | msgstr "**构建和测试:**"
337 |
338 | #: ../../contributing.rst:251
339 | msgid "Preview the result:"
340 | msgstr "预览结果:"
341 |
342 | #: ../../contributing.rst:259
343 | msgid "**Commit your changes**:"
344 | msgstr "**提交您的更改:**"
345 |
346 | #: ../../contributing.rst:266
347 | msgid "**Important Makefile Commands:**"
348 | msgstr "**重要的 Makefile 命令:**"
349 |
350 | #: ../../contributing.rst:268
351 | msgid ""
352 | "``make gettext``: Generate ``.pot`` template files from source ``.rst`` "
353 | "files"
354 | msgstr "``make gettext``:从源 ``.rst`` 文件生成 ``.pot`` 模板文件"
355 |
356 | #: ../../contributing.rst:269
357 | msgid "``make update-po``: Update ``.po`` files from ``.pot`` templates"
358 | msgstr "``make update-po``:从 ``.pot`` 模板更新 ``.po`` 文件"
359 |
360 | #: ../../contributing.rst:270
361 | msgid "``make html``: Build English documentation only"
362 | msgstr "``make html``:仅构建英文文档"
363 |
364 | #: ../../contributing.rst:271
365 | msgid "``make html-zh``: Build Chinese documentation only"
366 | msgstr "``make html-zh``:仅构建中文文档"
367 |
368 | #: ../../contributing.rst:272
369 | msgid "``make html-all``: Build all language versions"
370 | msgstr "``make html-all``:构建所有语言版本"
371 |
372 | #: ../../contributing.rst:274
373 | msgid "**Common Issues and Solutions:**"
374 | msgstr "**常见问题和解决方案:**"
375 |
376 | #: ../../contributing.rst:276
377 | msgid "**Translations not showing:**"
378 | msgstr "**翻译未显示:**"
379 |
380 | #: ../../contributing.rst:278
381 | msgid "Ensure ``.po`` files have non-empty ``msgstr`` values"
382 | msgstr "确保 ``.po`` 文件具有非空的 ``msgstr`` 值"
383 |
384 | #: ../../contributing.rst:279
385 | msgid "Rebuild with ``uv run make html-all``"
386 | msgstr "使用 ``uv run make html-all`` 重新构建"
387 |
388 | #: ../../contributing.rst:280
389 | msgid "Clear browser cache or use incognito mode"
390 | msgstr "清除浏览器缓存或使用隐身模式"
391 |
392 | #: ../../contributing.rst:282
393 | msgid "**New strings not appearing in .po files:**"
394 | msgstr "**新字符串未出现在 .po 文件中:**"
395 |
396 | #: ../../contributing.rst:284
397 | msgid "Run ``uv run make gettext`` to regenerate ``.pot`` files"
398 | msgstr "运行 ``uv run make gettext`` 重新生成 ``.pot`` 文件"
399 |
400 | #: ../../contributing.rst:285
401 | msgid "Run ``uv run make update-po`` to update ``.po`` files"
402 | msgstr "运行 ``uv run make update-po`` 更新 ``.po`` 文件"
403 |
404 | #: ../../contributing.rst:286
405 | msgid "Check that your source ``.rst`` files are included in the build"
406 | msgstr "检查您的源 ``.rst`` 文件是否包含在构建中"
407 |
408 | #: ../../contributing.rst:288
409 | msgid "**Language switcher not working:**"
410 | msgstr "**语言切换器不工作:**"
411 |
412 | #: ../../contributing.rst:290
413 | msgid "Verify ``_templates/language-switcher.html`` exists"
414 | msgstr "验证 ``_templates/language-switcher.html`` 存在"
415 |
416 | #: ../../contributing.rst:291
417 | msgid ""
418 | "Ensure target language HTML was built in correct subdirectory "
419 | "(``_build/html/zh_CN/``)"
420 | msgstr ""
421 | "确保目标语言 HTML 已构建在正确的子目录中 "
422 | "(``_build/html/zh_CN/``)"
423 |
424 | #: ../../contributing.rst:293
425 | msgid "**Best Practices:**"
426 | msgstr "**最佳实践:**"
427 |
428 | #: ../../contributing.rst:295
429 | msgid ""
430 | "**Commit ``.po`` files**: Always commit updated ``.po`` files to version "
431 | "control"
432 | msgstr "**提交 ``.po`` 文件**:始终将更新的 ``.po`` 文件提交到版本控制"
433 |
434 | #: ../../contributing.rst:296
435 | msgid ""
436 | "**Don't commit ``.pot`` files**: These are generated artifacts in "
437 | "``_build/gettext/``"
438 | msgstr ""
439 | "**不要提交 ``.pot`` 文件**:这些是 ``_build/gettext/`` 中生成的构建产物"
440 |
441 | #: ../../contributing.rst:297
442 | msgid ""
443 | "**Incremental translation**: It's okay to commit partially translated "
444 | "``.po`` files; untranslated strings display in English"
445 | msgstr ""
446 | "**增量翻译**:可以提交部分翻译的 ``.po`` 文件;未翻译的字符串将显示为英文"
447 |
448 | #: ../../contributing.rst:298
449 | msgid ""
450 | "**Review before push**: Build and preview locally before pushing "
451 | "translations"
452 | msgstr "**推送前审查**:推送翻译前在本地构建和预览"
453 |
454 | #: ../../contributing.rst:299
455 | msgid ""
456 | "**Consistent terminology**: Use consistent translations for technical "
457 | "terms across all pages"
458 | msgstr "**一致的术语**:在所有页面上对技术术语使用一致的翻译"
459 |
460 | #: ../../contributing.rst:300
461 | msgid ""
462 | "**Keep source in sync**: Run ``make update-po`` regularly to sync with "
463 | "source changes"
464 | msgstr "**保持源同步**:定期运行 ``make update-po`` 以与源更改同步"
465 |
466 | #: ../../contributing.rst:302
467 | msgid "**Automated Deployment:**"
468 | msgstr "**自动化部署:**"
469 |
470 | #: ../../contributing.rst:304
471 | msgid ""
472 | "Documentation is automatically built and deployed via GitHub Actions when"
473 | " pushed to ``main``:"
474 | msgstr "当推送到 ``main`` 分支时,文档将通过 GitHub Actions 自动构建和部署:"
475 |
476 | #: ../../contributing.rst:306
477 | msgid "Workflow: ``.github/workflows/docs.yml``"
478 | msgstr "工作流:``.github/workflows/docs.yml``"
479 |
480 | #: ../../contributing.rst:307
481 | msgid "Build command: ``uv run make html-all``"
482 | msgstr "构建命令:``uv run make html-all``"
483 |
484 | #: ../../contributing.rst:308
485 | msgid "Deployment: GitHub Pages at https://owenyou.github.io/dftt_timecode/"
486 | msgstr "部署:GitHub Pages 位于 https://owenyou.github.io/dftt_timecode/"
487 |
488 | #: ../../contributing.rst:310
489 | msgid ""
490 | "When you push translated ``.po`` files to the ``main`` branch (via "
491 | "``dev`` merge), the multilingual documentation is automatically rebuilt "
492 | "and deployed."
493 | msgstr ""
494 | "当您将翻译的 ``.po`` 文件推送到 ``main`` 分支时(通过 ``dev`` 合并),"
495 | "多语言文档将自动重新构建和部署。"
496 |
497 | #: ../../contributing.rst:312
498 | msgid "**Additional Resources:**"
499 | msgstr "**附加资源:**"
500 |
501 | #: ../../contributing.rst:314
502 | msgid ""
503 | "`Sphinx Internationalization `_"
505 | msgstr ""
506 | "`Sphinx 国际化 `_"
508 |
509 | #: ../../contributing.rst:315
510 | msgid "`sphinx-intl Documentation `_"
511 | msgstr "`sphinx-intl 文档 `_"
512 |
513 | #: ../../contributing.rst:316
514 | msgid ""
515 | "`GNU gettext Documentation "
516 | "`_"
517 | msgstr ""
518 | "`GNU gettext 文档 "
519 | "`_"
520 |
521 | #: ../../contributing.rst:319
522 | msgid "Submitting Changes"
523 | msgstr "提交更改"
524 |
525 | #: ../../contributing.rst:321
526 | msgid "Create a new branch for your changes:"
527 | msgstr "为您的更改创建新分支:"
528 |
529 | #: ../../contributing.rst:327
530 | msgid "Make your changes and commit:"
531 | msgstr "进行更改并提交:"
532 |
533 | #: ../../contributing.rst:334
534 | msgid "Push to your fork:"
535 | msgstr "推送到您的 Fork:"
536 |
537 | #: ../../contributing.rst:340
538 | msgid "Open a Pull Request on GitHub"
539 | msgstr "在 GitHub 上打开 Pull Request"
540 |
541 | #: ../../contributing.rst:343
542 | msgid "Pull Request Guidelines"
543 | msgstr "Pull Request 指南"
544 |
545 | #: ../../contributing.rst:345
546 | msgid "Provide a clear description of the changes"
547 | msgstr "提供更改的清晰描述"
548 |
549 | #: ../../contributing.rst:346
550 | msgid "Reference any related issues"
551 | msgstr "引用任何相关问题"
552 |
553 | #: ../../contributing.rst:347
554 | msgid "Ensure all tests pass"
555 | msgstr "确保所有测试通过"
556 |
557 | #: ../../contributing.rst:348
558 | msgid "Update documentation as needed"
559 | msgstr "根据需要更新文档"
560 |
561 | #: ../../contributing.rst:349
562 | msgid "Keep changes focused and atomic"
563 | msgstr "保持更改集中且原子化"
564 |
565 | #: ../../contributing.rst:352
566 | msgid "Reporting Bugs"
567 | msgstr "报告错误"
568 |
569 | #: ../../contributing.rst:354
570 | msgid "When reporting bugs, please include:"
571 | msgstr "报告错误时,请包含:"
572 |
573 | #: ../../contributing.rst:356
574 | msgid "Python version"
575 | msgstr "Python 版本"
576 |
577 | #: ../../contributing.rst:357
578 | msgid "dftt_timecode version"
579 | msgstr "dftt_timecode 版本"
580 |
581 | #: ../../contributing.rst:358
582 | msgid "Minimal code example that reproduces the issue"
583 | msgstr "能重现问题的最小代码示例"
584 |
585 | #: ../../contributing.rst:359
586 | msgid "Expected vs actual behavior"
587 | msgstr "预期行为与实际行为"
588 |
589 | #: ../../contributing.rst:360
590 | msgid "Any error messages or stack traces"
591 | msgstr "任何错误消息或堆栈跟踪"
592 |
593 | #: ../../contributing.rst:363
594 | msgid "Feature Requests"
595 | msgstr "功能请求"
596 |
597 | #: ../../contributing.rst:365
598 | msgid "Feature requests are welcome! Please provide:"
599 | msgstr "欢迎功能请求!请提供:"
600 |
601 | #: ../../contributing.rst:367
602 | msgid "Clear description of the feature"
603 | msgstr "功能的清晰描述"
604 |
605 | #: ../../contributing.rst:368
606 | msgid "Use cases and examples"
607 | msgstr "使用场景和示例"
608 |
609 | #: ../../contributing.rst:369
610 | msgid "Why this would be valuable to other users"
611 | msgstr "为什么这对其他用户有价值"
612 |
613 | #: ../../contributing.rst:372
614 | msgid "Code of Conduct"
615 | msgstr "行为准则"
616 |
617 | #: ../../contributing.rst:374
618 | msgid "Be respectful and inclusive"
619 | msgstr "保持尊重和包容"
620 |
621 | #: ../../contributing.rst:375
622 | msgid "Focus on constructive feedback"
623 | msgstr "专注于建设性反馈"
624 |
625 | #: ../../contributing.rst:376
626 | msgid "Help create a welcoming environment for all contributors"
627 | msgstr "帮助为所有贡献者创建一个友好的环境"
628 |
629 | #: ../../contributing.rst:379
630 | msgid "License"
631 | msgstr "许可证"
632 |
633 | #: ../../contributing.rst:381
634 | msgid ""
635 | "By contributing, you agree that your contributions will be licensed under"
636 | " the GNU Lesser General Public License v2 (LGPLv2)."
637 | msgstr "通过贡献,您同意您的贡献将按照 GNU Lesser General Public License v2 (LGPLv2) 授权。"
638 |
639 | #~ msgid ""
640 | #~ "For detailed information about the "
641 | #~ "translation system, see ``docs/I18N_README.md``."
642 | #~ msgstr "有关翻译系统的详细信息,请参阅 ``docs/I18N_README.md``。"
643 |
644 |
--------------------------------------------------------------------------------
/test/test_dftt_timerange.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from fractions import Fraction
3 | from dftt_timecode import DfttTimecode
4 | from dftt_timecode.core.dftt_timerange import DfttTimeRange
5 | from dftt_timecode.error import DFTTTimeRangeValueError, DFTTTimeRangeFPSError, DFTTTimeRangeMethodError
6 |
7 |
8 | class TestDfttTimeRangeInitialization:
9 | """Test timerange initialization with different methods"""
10 |
11 | def test_init_with_timecodes(self):
12 | """Test initialization with start and end timecodes"""
13 | start = DfttTimecode('00:00:00:00', fps=24)
14 | end = DfttTimecode('00:00:01:00', fps=24)
15 | tr = DfttTimeRange(start, end)
16 |
17 | assert tr.fps == 24
18 | assert tr.forward == True
19 | assert tr.duration == 1.0
20 | assert tr.framecount == 24
21 |
22 | def test_init_with_mixed_types(self):
23 | """Test initialization with mixed timecode and other types"""
24 | start = DfttTimecode('00:00:00:00', fps=24)
25 | tr = DfttTimeRange(start, 2.0)
26 |
27 | assert tr.fps == 24
28 | assert tr.duration == 2.0
29 |
30 | def test_init_with_precise_values(self):
31 | """Test initialization with precise_duration and start_precise_time"""
32 | tr = DfttTimeRange(
33 | start_precise_time=Fraction(10),
34 | precise_duration=Fraction(5),
35 | fps=24
36 | )
37 |
38 | assert tr.duration == 5.0
39 | assert tr.start_precise_time == Fraction(10)
40 | assert tr.precise_duration == Fraction(5)
41 |
42 | def test_init_reverse_direction(self):
43 | """Test initialization with reverse direction"""
44 | start = DfttTimecode('00:00:02:00', fps=24)
45 | end = DfttTimecode('00:00:00:00', fps=24)
46 | tr = DfttTimeRange(start, end, forward=False)
47 |
48 | assert tr.forward == False
49 | assert tr.duration == 2.0
50 |
51 | def test_init_fps_mismatch_error(self):
52 | """Test error when start and end have different FPS"""
53 | start = DfttTimecode('00:00:00:00', fps=24)
54 | end = DfttTimecode('00:00:01:00', fps=30)
55 |
56 | with pytest.raises(DFTTTimeRangeFPSError):
57 | DfttTimeRange(start, end)
58 |
59 | def test_init_zero_duration_error(self):
60 | """Test error when duration is zero"""
61 | start = DfttTimecode('00:00:01:00', fps=24)
62 | end = DfttTimecode('00:00:01:00', fps=24)
63 |
64 | with pytest.raises(DFTTTimeRangeValueError):
65 | DfttTimeRange(start, end)
66 |
67 | def test_init_strict_24h(self):
68 | """Test strict 24h mode"""
69 | tr = DfttTimeRange(
70 | start_precise_time=Fraction(0),
71 | precise_duration=Fraction(86400), # 24 hours
72 | fps=24,
73 | strict_24h=True
74 | )
75 |
76 | assert tr.strict_24h == True
77 | assert tr.duration == 86400
78 |
79 | def test_init_strict_24h_exceeded_error(self):
80 | """Test error when duration exceeds 24h in strict mode"""
81 | with pytest.raises(DFTTTimeRangeValueError):
82 | DfttTimeRange(
83 | start_precise_time=Fraction(0),
84 | precise_duration=Fraction(86401), # > 24 hours
85 | fps=24,
86 | strict_24h=True
87 | )
88 |
89 |
90 | class TestDfttTimeRangeProperties:
91 | """Test timerange properties"""
92 |
93 | def test_basic_properties(self):
94 | """Test basic property access"""
95 | tr = DfttTimeRange(
96 | start_precise_time=Fraction(10),
97 | precise_duration=Fraction(5),
98 | fps=25,
99 | forward=False
100 | )
101 |
102 | assert tr.fps == 25
103 | assert tr.forward == False
104 | assert tr.strict_24h == False
105 | assert tr.precise_duration == Fraction(5)
106 | assert tr.start_precise_time == Fraction(10)
107 |
108 | def test_end_precise_time_forward(self):
109 | """Test end_precise_time calculation for forward direction"""
110 | tr = DfttTimeRange(
111 | start_precise_time=Fraction(10),
112 | precise_duration=Fraction(5),
113 | forward=True
114 | )
115 |
116 | assert tr.end_precise_time == Fraction(15)
117 |
118 | def test_end_precise_time_reverse(self):
119 | """Test end_precise_time calculation for reverse direction"""
120 | tr = DfttTimeRange(
121 | start_precise_time=Fraction(10),
122 | precise_duration=Fraction(5),
123 | forward=False
124 | )
125 |
126 | assert tr.end_precise_time == Fraction(5)
127 |
128 | def test_duration_property(self):
129 | """Test duration property returns absolute value"""
130 | tr = DfttTimeRange(
131 | start_precise_time=Fraction(10),
132 | precise_duration=Fraction(-5),
133 | forward=False
134 | )
135 |
136 | assert tr.duration == 5.0
137 |
138 | def test_framecount_property(self):
139 | """Test framecount calculation"""
140 | tr = DfttTimeRange(
141 | start_precise_time=Fraction(0),
142 | precise_duration=Fraction(2),
143 | fps=24
144 | )
145 |
146 | assert tr.framecount == 48 # 2 seconds * 24 fps
147 |
148 | def test_start_end_timecode_properties(self):
149 | """Test start and end timecode properties"""
150 | tr = DfttTimeRange(
151 | start_precise_time=Fraction(1),
152 | precise_duration=Fraction(2),
153 | fps=24
154 | )
155 |
156 | start_tc = tr.start
157 | end_tc = tr.end
158 |
159 | assert isinstance(start_tc, DfttTimecode)
160 | assert isinstance(end_tc, DfttTimecode)
161 | assert start_tc.timestamp == 1.0
162 | assert end_tc.timestamp == 3.0
163 |
164 |
165 | class TestDfttTimeRangeCoreOperations:
166 | """Test core timerange operations"""
167 |
168 | def test_offset_with_number(self):
169 | """Test offset with numeric value"""
170 | tr = DfttTimeRange(
171 | start_precise_time=Fraction(10),
172 | precise_duration=Fraction(5),
173 | fps=24
174 | )
175 |
176 | offset_tr = tr.offset(2.0)
177 |
178 | assert offset_tr.start_precise_time == Fraction(12)
179 | assert offset_tr.precise_duration == Fraction(5)
180 | assert offset_tr.fps == 24
181 |
182 | def test_offset_with_timecode(self):
183 | """Test offset with timecode"""
184 | tr = DfttTimeRange(
185 | start_precise_time=Fraction(10),
186 | precise_duration=Fraction(5),
187 | fps=24
188 | )
189 |
190 | offset_tc = DfttTimecode('00:00:02:00', fps=24)
191 | offset_tr = tr.offset(offset_tc)
192 |
193 | assert offset_tr.start_precise_time == Fraction(12) # 10 + 2
194 |
195 | def test_extend_positive(self):
196 | """Test extending duration"""
197 | tr = DfttTimeRange(
198 | start_precise_time=Fraction(10),
199 | precise_duration=Fraction(5),
200 | fps=24
201 | )
202 |
203 | extended_tr = tr.extend(2)
204 |
205 | assert extended_tr.start_precise_time == Fraction(10)
206 | assert extended_tr.precise_duration == Fraction(7)
207 |
208 | def test_extend_negative(self):
209 | """Test extending with negative value (shortening)"""
210 | tr = DfttTimeRange(
211 | start_precise_time=Fraction(10),
212 | precise_duration=Fraction(5),
213 | fps=24
214 | )
215 |
216 | extended_tr = tr.extend(-2)
217 |
218 | assert extended_tr.start_precise_time == Fraction(10)
219 | assert extended_tr.precise_duration == Fraction(3)
220 |
221 | def test_extend_to_zero_error(self):
222 | """Test error when extending to zero duration"""
223 | tr = DfttTimeRange(
224 | start_precise_time=Fraction(10),
225 | precise_duration=Fraction(5),
226 | fps=24
227 | )
228 |
229 | with pytest.raises(DFTTTimeRangeValueError):
230 | tr.extend(-5)
231 |
232 | def test_shorten(self):
233 | """Test shortening duration"""
234 | tr = DfttTimeRange(
235 | start_precise_time=Fraction(10),
236 | precise_duration=Fraction(5),
237 | fps=24
238 | )
239 |
240 | shortened_tr = tr.shorten(2)
241 |
242 | assert shortened_tr.start_precise_time == Fraction(10)
243 | assert shortened_tr.precise_duration == Fraction(3)
244 |
245 | def test_reverse(self):
246 | """Test reversing direction"""
247 | tr = DfttTimeRange(
248 | start_precise_time=Fraction(10),
249 | precise_duration=Fraction(5),
250 | fps=24,
251 | forward=True
252 | )
253 |
254 | reversed_tr = tr.reverse()
255 |
256 | assert reversed_tr.start_precise_time == Fraction(15) # original end
257 | assert reversed_tr.precise_duration == Fraction(5)
258 | assert reversed_tr.forward == False
259 |
260 | def test_retime_factor(self):
261 | """Test retime with factor"""
262 | tr = DfttTimeRange(
263 | start_precise_time=Fraction(10),
264 | precise_duration=Fraction(4),
265 | fps=24
266 | )
267 |
268 | retimed_tr = tr.retime(0.5)
269 |
270 | assert retimed_tr.start_precise_time == Fraction(10)
271 | assert retimed_tr.precise_duration == Fraction(2)
272 |
273 | def test_retime_zero_error(self):
274 | """Test error when retime factor is zero"""
275 | tr = DfttTimeRange(
276 | start_precise_time=Fraction(10),
277 | precise_duration=Fraction(4),
278 | fps=24
279 | )
280 |
281 | with pytest.raises(DFTTTimeRangeValueError):
282 | tr.retime(0)
283 |
284 | def test_separate_into_parts(self):
285 | """Test separating timerange into parts"""
286 | tr = DfttTimeRange(
287 | start_precise_time=Fraction(10),
288 | precise_duration=Fraction(6),
289 | fps=24
290 | )
291 |
292 | parts = tr.separate(3)
293 |
294 | assert len(parts) == 3
295 | assert all(isinstance(part, DfttTimeRange) for part in parts)
296 | assert all(part.precise_duration == Fraction(2) for part in parts)
297 | assert parts[0].start_precise_time == Fraction(10)
298 | assert parts[1].start_precise_time == Fraction(12)
299 | assert parts[2].start_precise_time == Fraction(14)
300 |
301 | def test_separate_too_few_parts_error(self):
302 | """Test error when separating into too few parts"""
303 | tr = DfttTimeRange(
304 | start_precise_time=Fraction(10),
305 | precise_duration=Fraction(6),
306 | fps=24
307 | )
308 |
309 | with pytest.raises(DFTTTimeRangeValueError):
310 | tr.separate(1)
311 |
312 |
313 | class TestDfttTimeRangeContains:
314 | """Test contains functionality"""
315 |
316 | def test_contains_timecode_forward(self):
317 | """Test contains with timecode in forward direction"""
318 | tr = DfttTimeRange(
319 | start_precise_time=Fraction(10),
320 | precise_duration=Fraction(5),
321 | fps=24,
322 | forward=True
323 | )
324 |
325 | tc_inside = DfttTimecode(12.0, fps=24)
326 | tc_outside = DfttTimecode(16.0, fps=24)
327 |
328 | assert tr.contains(tc_inside) == True
329 | assert tr.contains(tc_outside) == False
330 |
331 | def test_contains_timecode_reverse(self):
332 | """Test contains with timecode in reverse direction"""
333 | tr = DfttTimeRange(
334 | start_precise_time=Fraction(10),
335 | precise_duration=Fraction(5),
336 | fps=24,
337 | forward=False
338 | )
339 |
340 | tc_inside = DfttTimecode(8.0, fps=24) # between 5 and 10
341 | tc_outside = DfttTimecode(12.0, fps=24)
342 |
343 | assert tr.contains(tc_inside) == True
344 | assert tr.contains(tc_outside) == False
345 |
346 | def test_contains_timerange(self):
347 | """Test contains with another timerange"""
348 | tr1 = DfttTimeRange(
349 | start_precise_time=Fraction(10),
350 | precise_duration=Fraction(10),
351 | fps=24
352 | )
353 |
354 | tr2 = DfttTimeRange(
355 | start_precise_time=Fraction(12),
356 | precise_duration=Fraction(2),
357 | fps=24
358 | )
359 |
360 | assert tr1.contains(tr2) == True
361 | assert tr2.contains(tr1) == False
362 |
363 | def test_contains_string_input(self):
364 | """Test contains with string input"""
365 | tr = DfttTimeRange(
366 | start_precise_time=Fraction(10),
367 | precise_duration=Fraction(5),
368 | fps=24
369 | )
370 |
371 | assert tr.contains('12.0s') == True
372 | assert tr.contains('16.0s') == False
373 |
374 |
375 | class TestDfttTimeRangeOperations:
376 | """Test operations between timeranges"""
377 |
378 | def test_intersect_overlapping(self):
379 | """Test intersection of overlapping timeranges"""
380 | tr1 = DfttTimeRange(
381 | start_precise_time=Fraction(10),
382 | precise_duration=Fraction(10),
383 | fps=24
384 | )
385 |
386 | tr2 = DfttTimeRange(
387 | start_precise_time=Fraction(15),
388 | precise_duration=Fraction(10),
389 | fps=24
390 | )
391 |
392 | intersection = tr1.intersect(tr2)
393 |
394 | assert intersection is not None
395 | assert intersection.start_precise_time == Fraction(15)
396 | assert intersection.precise_duration == Fraction(5)
397 |
398 | def test_intersect_non_overlapping(self):
399 | """Test intersection of non-overlapping timeranges"""
400 | tr1 = DfttTimeRange(
401 | start_precise_time=Fraction(10),
402 | precise_duration=Fraction(5),
403 | fps=24
404 | )
405 |
406 | tr2 = DfttTimeRange(
407 | start_precise_time=Fraction(20),
408 | precise_duration=Fraction(5),
409 | fps=24
410 | )
411 |
412 | intersection = tr1.intersect(tr2)
413 |
414 | assert intersection is None
415 |
416 | def test_intersect_different_direction_error(self):
417 | """Test error when intersecting different directions"""
418 | tr1 = DfttTimeRange(
419 | start_precise_time=Fraction(10),
420 | precise_duration=Fraction(5),
421 | fps=24,
422 | forward=True
423 | )
424 |
425 | tr2 = DfttTimeRange(
426 | start_precise_time=Fraction(15),
427 | precise_duration=Fraction(5),
428 | fps=24,
429 | forward=False
430 | )
431 |
432 | with pytest.raises(DFTTTimeRangeMethodError):
433 | tr1.intersect(tr2)
434 |
435 | def test_union_adjacent(self):
436 | """Test union of adjacent timeranges"""
437 | tr1 = DfttTimeRange(
438 | start_precise_time=Fraction(10),
439 | precise_duration=Fraction(5),
440 | fps=24
441 | )
442 |
443 | tr2 = DfttTimeRange(
444 | start_precise_time=Fraction(15),
445 | precise_duration=Fraction(5),
446 | fps=24
447 | )
448 |
449 | union = tr1.union(tr2)
450 |
451 | assert union.start_precise_time == Fraction(10)
452 | assert union.precise_duration == Fraction(10)
453 |
454 | def test_union_overlapping(self):
455 | """Test union of overlapping timeranges"""
456 | tr1 = DfttTimeRange(
457 | start_precise_time=Fraction(10),
458 | precise_duration=Fraction(10),
459 | fps=24
460 | )
461 |
462 | tr2 = DfttTimeRange(
463 | start_precise_time=Fraction(15),
464 | precise_duration=Fraction(10),
465 | fps=24
466 | )
467 |
468 | union = tr1.union(tr2)
469 |
470 | assert union.start_precise_time == Fraction(10)
471 | assert union.precise_duration == Fraction(15)
472 |
473 | def test_add_same_direction(self):
474 | """Test adding timeranges with same direction"""
475 | tr1 = DfttTimeRange(
476 | start_precise_time=Fraction(10),
477 | precise_duration=Fraction(5),
478 | fps=24,
479 | forward=True
480 | )
481 |
482 | tr2 = DfttTimeRange(
483 | start_precise_time=Fraction(20),
484 | precise_duration=Fraction(3),
485 | fps=24,
486 | forward=True
487 | )
488 |
489 | result = tr1.add(tr2)
490 |
491 | assert result.start_precise_time == Fraction(10)
492 | assert result.precise_duration == Fraction(8)
493 |
494 | def test_add_different_direction(self):
495 | """Test adding timeranges with different directions"""
496 | tr1 = DfttTimeRange(
497 | start_precise_time=Fraction(10),
498 | precise_duration=Fraction(5),
499 | fps=24,
500 | forward=True
501 | )
502 |
503 | tr2 = DfttTimeRange(
504 | start_precise_time=Fraction(20),
505 | precise_duration=Fraction(3),
506 | fps=24,
507 | forward=False
508 | )
509 |
510 | result = tr1.add(tr2)
511 |
512 | assert result.start_precise_time == Fraction(10)
513 | assert result.precise_duration == Fraction(2) # 5 - 3
514 |
515 | def test_subtract_same_direction(self):
516 | """Test subtracting timeranges with same direction"""
517 | tr1 = DfttTimeRange(
518 | start_precise_time=Fraction(10),
519 | precise_duration=Fraction(5),
520 | fps=24,
521 | forward=True
522 | )
523 |
524 | tr2 = DfttTimeRange(
525 | start_precise_time=Fraction(20),
526 | precise_duration=Fraction(3),
527 | fps=24,
528 | forward=True
529 | )
530 |
531 | result = tr1.subtract(tr2)
532 |
533 | assert result.start_precise_time == Fraction(10)
534 | assert result.precise_duration == Fraction(2) # 5 - 3
535 |
536 |
537 | class TestDfttTimeRangeMagicMethods:
538 | """Test magic methods and operators"""
539 |
540 | def test_str_representation(self):
541 | """Test string representation"""
542 | tr = DfttTimeRange(
543 | start_precise_time=Fraction(10),
544 | precise_duration=Fraction(5),
545 | fps=24
546 | )
547 |
548 | str_repr = str(tr)
549 | assert 'DfttTimeRange' in str_repr
550 | assert 'fps=24' in str_repr
551 |
552 | def test_len(self):
553 | """Test len() returns framecount"""
554 | tr = DfttTimeRange(
555 | start_precise_time=Fraction(0),
556 | precise_duration=Fraction(2),
557 | fps=24
558 | )
559 |
560 | assert len(tr) == 48
561 |
562 | def test_contains_magic_method(self):
563 | """Test __contains__ magic method"""
564 | tr = DfttTimeRange(
565 | start_precise_time=Fraction(10),
566 | precise_duration=Fraction(5),
567 | fps=24
568 | )
569 |
570 | tc = DfttTimecode(12.0, fps=24)
571 |
572 | assert tc in tr
573 |
574 | def test_iteration(self):
575 | """Test iteration through timerange"""
576 | tr = DfttTimeRange(
577 | start_precise_time=Fraction(0),
578 | precise_duration=Fraction(1), # 1 second
579 | fps=2 # 2 fps for easier testing
580 | )
581 |
582 | timecodes = list(tr)
583 |
584 | assert len(timecodes) == 2 # 1 second * 2 fps
585 | assert all(isinstance(tc, DfttTimecode) for tc in timecodes)
586 |
587 | def test_add_operator_with_timerange(self):
588 | """Test + operator with timerange"""
589 | tr1 = DfttTimeRange(
590 | start_precise_time=Fraction(10),
591 | precise_duration=Fraction(5),
592 | fps=24
593 | )
594 |
595 | tr2 = DfttTimeRange(
596 | start_precise_time=Fraction(20),
597 | precise_duration=Fraction(3),
598 | fps=24
599 | )
600 |
601 | result = tr1 + tr2
602 |
603 | assert result.precise_duration == Fraction(8)
604 |
605 | def test_add_operator_with_offset(self):
606 | """Test + operator with numeric offset"""
607 | tr = DfttTimeRange(
608 | start_precise_time=Fraction(10),
609 | precise_duration=Fraction(5),
610 | fps=24
611 | )
612 |
613 | result = tr + 2.0
614 |
615 | assert result.start_precise_time == Fraction(12)
616 |
617 | def test_sub_operator_with_timerange(self):
618 | """Test - operator with timerange"""
619 | tr1 = DfttTimeRange(
620 | start_precise_time=Fraction(10),
621 | precise_duration=Fraction(5),
622 | fps=24
623 | )
624 |
625 | tr2 = DfttTimeRange(
626 | start_precise_time=Fraction(20),
627 | precise_duration=Fraction(3),
628 | fps=24
629 | )
630 |
631 | result = tr1 - tr2
632 |
633 | assert result.precise_duration == Fraction(2)
634 |
635 | def test_mul_operator(self):
636 | """Test * operator for retime"""
637 | tr = DfttTimeRange(
638 | start_precise_time=Fraction(10),
639 | precise_duration=Fraction(4),
640 | fps=24
641 | )
642 |
643 | result = tr * 2
644 |
645 | assert result.precise_duration == Fraction(8)
646 |
647 | def test_truediv_operator(self):
648 | """Test / operator for retime"""
649 | tr = DfttTimeRange(
650 | start_precise_time=Fraction(10),
651 | precise_duration=Fraction(4),
652 | fps=24
653 | )
654 |
655 | result = tr / 2
656 |
657 | assert result.precise_duration == Fraction(2)
658 |
659 | def test_and_operator(self):
660 | """Test & operator for intersection"""
661 | tr1 = DfttTimeRange(
662 | start_precise_time=Fraction(10),
663 | precise_duration=Fraction(10),
664 | fps=24
665 | )
666 |
667 | tr2 = DfttTimeRange(
668 | start_precise_time=Fraction(15),
669 | precise_duration=Fraction(10),
670 | fps=24
671 | )
672 |
673 | result = tr1 & tr2
674 |
675 | assert result is not None
676 | assert result.precise_duration == Fraction(5)
677 |
678 | def test_or_operator(self):
679 | """Test | operator for union"""
680 | tr1 = DfttTimeRange(
681 | start_precise_time=Fraction(10),
682 | precise_duration=Fraction(5),
683 | fps=24
684 | )
685 |
686 | tr2 = DfttTimeRange(
687 | start_precise_time=Fraction(15),
688 | precise_duration=Fraction(5),
689 | fps=24
690 | )
691 |
692 | result = tr1 | tr2
693 |
694 | assert result.precise_duration == Fraction(10)
695 |
696 | def test_equality_comparison(self):
697 | """Test equality comparison"""
698 | tr1 = DfttTimeRange(
699 | start_precise_time=Fraction(10),
700 | precise_duration=Fraction(5),
701 | fps=24
702 | )
703 |
704 | tr2 = DfttTimeRange(
705 | start_precise_time=Fraction(10),
706 | precise_duration=Fraction(5),
707 | fps=24
708 | )
709 |
710 | tr3 = DfttTimeRange(
711 | start_precise_time=Fraction(10),
712 | precise_duration=Fraction(6),
713 | fps=24
714 | )
715 |
716 | assert tr1 == tr2
717 | assert tr1 != tr3
718 |
719 | def test_comparison_operators(self):
720 | """Test comparison operators"""
721 | tr1 = DfttTimeRange(
722 | start_precise_time=Fraction(10),
723 | precise_duration=Fraction(5),
724 | fps=24
725 | )
726 |
727 | tr2 = DfttTimeRange(
728 | start_precise_time=Fraction(15),
729 | precise_duration=Fraction(5),
730 | fps=24
731 | )
732 |
733 | assert tr1 < tr2
734 | assert tr1 <= tr2
735 | assert tr2 > tr1
736 | assert tr2 >= tr1
737 |
738 |
739 | class TestDfttTimeRangeEdgeCases:
740 | """Test edge cases and error conditions"""
741 |
742 | def test_midnight_crossing_strict_mode(self):
743 | """Test midnight crossing in strict mode"""
744 | start = DfttTimecode('23:59:59:00', fps=24)
745 | end = DfttTimecode('00:00:01:00', fps=24)
746 |
747 | tr = DfttTimeRange(start, end, strict_24h=True)
748 |
749 | # Should handle midnight crossing correctly
750 | assert tr.duration == 2.0 # 1 second before + 1 second after midnight
751 |
752 | def test_very_small_duration(self):
753 | """Test very small duration handling"""
754 | tr = DfttTimeRange(
755 | start_precise_time=Fraction(0),
756 | precise_duration=Fraction(1, 1000), # 1ms
757 | fps=24
758 | )
759 |
760 | assert tr.duration == 0.001
761 |
762 | def test_high_fps_precision(self):
763 | """Test high FPS precision"""
764 | tr = DfttTimeRange(
765 | start_precise_time=Fraction(0),
766 | precise_duration=Fraction(1),
767 | fps=999.99 # high fps
768 | )
769 |
770 | assert tr.framecount == 1000 # approximately
771 |
772 | def test_fractional_fps(self):
773 | """Test fractional FPS"""
774 | tr = DfttTimeRange(
775 | start_precise_time=Fraction(0),
776 | precise_duration=Fraction(2),
777 | fps=23.976 # 23.976 fps
778 | )
779 |
780 | expected_frames = int(round(2 * 23.976))
781 | assert tr.framecount == expected_frames
782 |
783 |
784 | if __name__ == '__main__':
785 | pytest.main([__file__, '-v'])
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 2.1, February 1999
3 |
4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc.
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | [This is the first released version of the Lesser GPL. It also counts
10 | as the successor of the GNU Library Public License, version 2, hence
11 | the version number 2.1.]
12 |
13 | Preamble
14 |
15 | The licenses for most software are designed to take away your
16 | freedom to share and change it. By contrast, the GNU General Public
17 | Licenses are intended to guarantee your freedom to share and change
18 | free software--to make sure the software is free for all its users.
19 |
20 | This license, the Lesser General Public License, applies to some
21 | specially designated software packages--typically libraries--of the
22 | Free Software Foundation and other authors who decide to use it. You
23 | can use it too, but we suggest you first think carefully about whether
24 | this license or the ordinary General Public License is the better
25 | strategy to use in any particular case, based on the explanations below.
26 |
27 | When we speak of free software, we are referring to freedom of use,
28 | not price. Our General Public Licenses are designed to make sure that
29 | you have the freedom to distribute copies of free software (and charge
30 | for this service if you wish); that you receive source code or can get
31 | it if you want it; that you can change the software and use pieces of
32 | it in new free programs; and that you are informed that you can do
33 | these things.
34 |
35 | To protect your rights, we need to make restrictions that forbid
36 | distributors to deny you these rights or to ask you to surrender these
37 | rights. These restrictions translate to certain responsibilities for
38 | you if you distribute copies of the library or if you modify it.
39 |
40 | For example, if you distribute copies of the library, whether gratis
41 | or for a fee, you must give the recipients all the rights that we gave
42 | you. You must make sure that they, too, receive or can get the source
43 | code. If you link other code with the library, you must provide
44 | complete object files to the recipients, so that they can relink them
45 | with the library after making changes to the library and recompiling
46 | it. And you must show them these terms so they know their rights.
47 |
48 | We protect your rights with a two-step method: (1) we copyright the
49 | library, and (2) we offer you this license, which gives you legal
50 | permission to copy, distribute and/or modify the library.
51 |
52 | To protect each distributor, we want to make it very clear that
53 | there is no warranty for the free library. Also, if the library is
54 | modified by someone else and passed on, the recipients should know
55 | that what they have is not the original version, so that the original
56 | author's reputation will not be affected by problems that might be
57 | introduced by others.
58 |
59 | Finally, software patents pose a constant threat to the existence of
60 | any free program. We wish to make sure that a company cannot
61 | effectively restrict the users of a free program by obtaining a
62 | restrictive license from a patent holder. Therefore, we insist that
63 | any patent license obtained for a version of the library must be
64 | consistent with the full freedom of use specified in this license.
65 |
66 | Most GNU software, including some libraries, is covered by the
67 | ordinary GNU General Public License. This license, the GNU Lesser
68 | General Public License, applies to certain designated libraries, and
69 | is quite different from the ordinary General Public License. We use
70 | this license for certain libraries in order to permit linking those
71 | libraries into non-free programs.
72 |
73 | When a program is linked with a library, whether statically or using
74 | a shared library, the combination of the two is legally speaking a
75 | combined work, a derivative of the original library. The ordinary
76 | General Public License therefore permits such linking only if the
77 | entire combination fits its criteria of freedom. The Lesser General
78 | Public License permits more lax criteria for linking other code with
79 | the library.
80 |
81 | We call this license the "Lesser" General Public License because it
82 | does Less to protect the user's freedom than the ordinary General
83 | Public License. It also provides other free software developers Less
84 | of an advantage over competing non-free programs. These disadvantages
85 | are the reason we use the ordinary General Public License for many
86 | libraries. However, the Lesser license provides advantages in certain
87 | special circumstances.
88 |
89 | For example, on rare occasions, there may be a special need to
90 | encourage the widest possible use of a certain library, so that it becomes
91 | a de-facto standard. To achieve this, non-free programs must be
92 | allowed to use the library. A more frequent case is that a free
93 | library does the same job as widely used non-free libraries. In this
94 | case, there is little to gain by limiting the free library to free
95 | software only, so we use the Lesser General Public License.
96 |
97 | In other cases, permission to use a particular library in non-free
98 | programs enables a greater number of people to use a large body of
99 | free software. For example, permission to use the GNU C Library in
100 | non-free programs enables many more people to use the whole GNU
101 | operating system, as well as its variant, the GNU/Linux operating
102 | system.
103 |
104 | Although the Lesser General Public License is Less protective of the
105 | users' freedom, it does ensure that the user of a program that is
106 | linked with the Library has the freedom and the wherewithal to run
107 | that program using a modified version of the Library.
108 |
109 | The precise terms and conditions for copying, distribution and
110 | modification follow. Pay close attention to the difference between a
111 | "work based on the library" and a "work that uses the library". The
112 | former contains code derived from the library, whereas the latter must
113 | be combined with the library in order to run.
114 |
115 | GNU LESSER GENERAL PUBLIC LICENSE
116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
117 |
118 | 0. This License Agreement applies to any software library or other
119 | program which contains a notice placed by the copyright holder or
120 | other authorized party saying it may be distributed under the terms of
121 | this Lesser General Public License (also called "this License").
122 | Each licensee is addressed as "you".
123 |
124 | A "library" means a collection of software functions and/or data
125 | prepared so as to be conveniently linked with application programs
126 | (which use some of those functions and data) to form executables.
127 |
128 | The "Library", below, refers to any such software library or work
129 | which has been distributed under these terms. A "work based on the
130 | Library" means either the Library or any derivative work under
131 | copyright law: that is to say, a work containing the Library or a
132 | portion of it, either verbatim or with modifications and/or translated
133 | straightforwardly into another language. (Hereinafter, translation is
134 | included without limitation in the term "modification".)
135 |
136 | "Source code" for a work means the preferred form of the work for
137 | making modifications to it. For a library, complete source code means
138 | all the source code for all modules it contains, plus any associated
139 | interface definition files, plus the scripts used to control compilation
140 | and installation of the library.
141 |
142 | Activities other than copying, distribution and modification are not
143 | covered by this License; they are outside its scope. The act of
144 | running a program using the Library is not restricted, and output from
145 | such a program is covered only if its contents constitute a work based
146 | on the Library (independent of the use of the Library in a tool for
147 | writing it). Whether that is true depends on what the Library does
148 | and what the program that uses the Library does.
149 |
150 | 1. You may copy and distribute verbatim copies of the Library's
151 | complete source code as you receive it, in any medium, provided that
152 | you conspicuously and appropriately publish on each copy an
153 | appropriate copyright notice and disclaimer of warranty; keep intact
154 | all the notices that refer to this License and to the absence of any
155 | warranty; and distribute a copy of this License along with the
156 | Library.
157 |
158 | You may charge a fee for the physical act of transferring a copy,
159 | and you may at your option offer warranty protection in exchange for a
160 | fee.
161 |
162 | 2. You may modify your copy or copies of the Library or any portion
163 | of it, thus forming a work based on the Library, and copy and
164 | distribute such modifications or work under the terms of Section 1
165 | above, provided that you also meet all of these conditions:
166 |
167 | a) The modified work must itself be a software library.
168 |
169 | b) You must cause the files modified to carry prominent notices
170 | stating that you changed the files and the date of any change.
171 |
172 | c) You must cause the whole of the work to be licensed at no
173 | charge to all third parties under the terms of this License.
174 |
175 | d) If a facility in the modified Library refers to a function or a
176 | table of data to be supplied by an application program that uses
177 | the facility, other than as an argument passed when the facility
178 | is invoked, then you must make a good faith effort to ensure that,
179 | in the event an application does not supply such function or
180 | table, the facility still operates, and performs whatever part of
181 | its purpose remains meaningful.
182 |
183 | (For example, a function in a library to compute square roots has
184 | a purpose that is entirely well-defined independent of the
185 | application. Therefore, Subsection 2d requires that any
186 | application-supplied function or table used by this function must
187 | be optional: if the application does not supply it, the square
188 | root function must still compute square roots.)
189 |
190 | These requirements apply to the modified work as a whole. If
191 | identifiable sections of that work are not derived from the Library,
192 | and can be reasonably considered independent and separate works in
193 | themselves, then this License, and its terms, do not apply to those
194 | sections when you distribute them as separate works. But when you
195 | distribute the same sections as part of a whole which is a work based
196 | on the Library, the distribution of the whole must be on the terms of
197 | this License, whose permissions for other licensees extend to the
198 | entire whole, and thus to each and every part regardless of who wrote
199 | it.
200 |
201 | Thus, it is not the intent of this section to claim rights or contest
202 | your rights to work written entirely by you; rather, the intent is to
203 | exercise the right to control the distribution of derivative or
204 | collective works based on the Library.
205 |
206 | In addition, mere aggregation of another work not based on the Library
207 | with the Library (or with a work based on the Library) on a volume of
208 | a storage or distribution medium does not bring the other work under
209 | the scope of this License.
210 |
211 | 3. You may opt to apply the terms of the ordinary GNU General Public
212 | License instead of this License to a given copy of the Library. To do
213 | this, you must alter all the notices that refer to this License, so
214 | that they refer to the ordinary GNU General Public License, version 2,
215 | instead of to this License. (If a newer version than version 2 of the
216 | ordinary GNU General Public License has appeared, then you can specify
217 | that version instead if you wish.) Do not make any other change in
218 | these notices.
219 |
220 | Once this change is made in a given copy, it is irreversible for
221 | that copy, so the ordinary GNU General Public License applies to all
222 | subsequent copies and derivative works made from that copy.
223 |
224 | This option is useful when you wish to copy part of the code of
225 | the Library into a program that is not a library.
226 |
227 | 4. You may copy and distribute the Library (or a portion or
228 | derivative of it, under Section 2) in object code or executable form
229 | under the terms of Sections 1 and 2 above provided that you accompany
230 | it with the complete corresponding machine-readable source code, which
231 | must be distributed under the terms of Sections 1 and 2 above on a
232 | medium customarily used for software interchange.
233 |
234 | If distribution of object code is made by offering access to copy
235 | from a designated place, then offering equivalent access to copy the
236 | source code from the same place satisfies the requirement to
237 | distribute the source code, even though third parties are not
238 | compelled to copy the source along with the object code.
239 |
240 | 5. A program that contains no derivative of any portion of the
241 | Library, but is designed to work with the Library by being compiled or
242 | linked with it, is called a "work that uses the Library". Such a
243 | work, in isolation, is not a derivative work of the Library, and
244 | therefore falls outside the scope of this License.
245 |
246 | However, linking a "work that uses the Library" with the Library
247 | creates an executable that is a derivative of the Library (because it
248 | contains portions of the Library), rather than a "work that uses the
249 | library". The executable is therefore covered by this License.
250 | Section 6 states terms for distribution of such executables.
251 |
252 | When a "work that uses the Library" uses material from a header file
253 | that is part of the Library, the object code for the work may be a
254 | derivative work of the Library even though the source code is not.
255 | Whether this is true is especially significant if the work can be
256 | linked without the Library, or if the work is itself a library. The
257 | threshold for this to be true is not precisely defined by law.
258 |
259 | If such an object file uses only numerical parameters, data
260 | structure layouts and accessors, and small macros and small inline
261 | functions (ten lines or less in length), then the use of the object
262 | file is unrestricted, regardless of whether it is legally a derivative
263 | work. (Executables containing this object code plus portions of the
264 | Library will still fall under Section 6.)
265 |
266 | Otherwise, if the work is a derivative of the Library, you may
267 | distribute the object code for the work under the terms of Section 6.
268 | Any executables containing that work also fall under Section 6,
269 | whether or not they are linked directly with the Library itself.
270 |
271 | 6. As an exception to the Sections above, you may also combine or
272 | link a "work that uses the Library" with the Library to produce a
273 | work containing portions of the Library, and distribute that work
274 | under terms of your choice, provided that the terms permit
275 | modification of the work for the customer's own use and reverse
276 | engineering for debugging such modifications.
277 |
278 | You must give prominent notice with each copy of the work that the
279 | Library is used in it and that the Library and its use are covered by
280 | this License. You must supply a copy of this License. If the work
281 | during execution displays copyright notices, you must include the
282 | copyright notice for the Library among them, as well as a reference
283 | directing the user to the copy of this License. Also, you must do one
284 | of these things:
285 |
286 | a) Accompany the work with the complete corresponding
287 | machine-readable source code for the Library including whatever
288 | changes were used in the work (which must be distributed under
289 | Sections 1 and 2 above); and, if the work is an executable linked
290 | with the Library, with the complete machine-readable "work that
291 | uses the Library", as object code and/or source code, so that the
292 | user can modify the Library and then relink to produce a modified
293 | executable containing the modified Library. (It is understood
294 | that the user who changes the contents of definitions files in the
295 | Library will not necessarily be able to recompile the application
296 | to use the modified definitions.)
297 |
298 | b) Use a suitable shared library mechanism for linking with the
299 | Library. A suitable mechanism is one that (1) uses at run time a
300 | copy of the library already present on the user's computer system,
301 | rather than copying library functions into the executable, and (2)
302 | will operate properly with a modified version of the library, if
303 | the user installs one, as long as the modified version is
304 | interface-compatible with the version that the work was made with.
305 |
306 | c) Accompany the work with a written offer, valid for at
307 | least three years, to give the same user the materials
308 | specified in Subsection 6a, above, for a charge no more
309 | than the cost of performing this distribution.
310 |
311 | d) If distribution of the work is made by offering access to copy
312 | from a designated place, offer equivalent access to copy the above
313 | specified materials from the same place.
314 |
315 | e) Verify that the user has already received a copy of these
316 | materials or that you have already sent this user a copy.
317 |
318 | For an executable, the required form of the "work that uses the
319 | Library" must include any data and utility programs needed for
320 | reproducing the executable from it. However, as a special exception,
321 | the materials to be distributed need not include anything that is
322 | normally distributed (in either source or binary form) with the major
323 | components (compiler, kernel, and so on) of the operating system on
324 | which the executable runs, unless that component itself accompanies
325 | the executable.
326 |
327 | It may happen that this requirement contradicts the license
328 | restrictions of other proprietary libraries that do not normally
329 | accompany the operating system. Such a contradiction means you cannot
330 | use both them and the Library together in an executable that you
331 | distribute.
332 |
333 | 7. You may place library facilities that are a work based on the
334 | Library side-by-side in a single library together with other library
335 | facilities not covered by this License, and distribute such a combined
336 | library, provided that the separate distribution of the work based on
337 | the Library and of the other library facilities is otherwise
338 | permitted, and provided that you do these two things:
339 |
340 | a) Accompany the combined library with a copy of the same work
341 | based on the Library, uncombined with any other library
342 | facilities. This must be distributed under the terms of the
343 | Sections above.
344 |
345 | b) Give prominent notice with the combined library of the fact
346 | that part of it is a work based on the Library, and explaining
347 | where to find the accompanying uncombined form of the same work.
348 |
349 | 8. You may not copy, modify, sublicense, link with, or distribute
350 | the Library except as expressly provided under this License. Any
351 | attempt otherwise to copy, modify, sublicense, link with, or
352 | distribute the Library is void, and will automatically terminate your
353 | rights under this License. However, parties who have received copies,
354 | or rights, from you under this License will not have their licenses
355 | terminated so long as such parties remain in full compliance.
356 |
357 | 9. You are not required to accept this License, since you have not
358 | signed it. However, nothing else grants you permission to modify or
359 | distribute the Library or its derivative works. These actions are
360 | prohibited by law if you do not accept this License. Therefore, by
361 | modifying or distributing the Library (or any work based on the
362 | Library), you indicate your acceptance of this License to do so, and
363 | all its terms and conditions for copying, distributing or modifying
364 | the Library or works based on it.
365 |
366 | 10. Each time you redistribute the Library (or any work based on the
367 | Library), the recipient automatically receives a license from the
368 | original licensor to copy, distribute, link with or modify the Library
369 | subject to these terms and conditions. You may not impose any further
370 | restrictions on the recipients' exercise of the rights granted herein.
371 | You are not responsible for enforcing compliance by third parties with
372 | this License.
373 |
374 | 11. If, as a consequence of a court judgment or allegation of patent
375 | infringement or for any other reason (not limited to patent issues),
376 | conditions are imposed on you (whether by court order, agreement or
377 | otherwise) that contradict the conditions of this License, they do not
378 | excuse you from the conditions of this License. If you cannot
379 | distribute so as to satisfy simultaneously your obligations under this
380 | License and any other pertinent obligations, then as a consequence you
381 | may not distribute the Library at all. For example, if a patent
382 | license would not permit royalty-free redistribution of the Library by
383 | all those who receive copies directly or indirectly through you, then
384 | the only way you could satisfy both it and this License would be to
385 | refrain entirely from distribution of the Library.
386 |
387 | If any portion of this section is held invalid or unenforceable under any
388 | particular circumstance, the balance of the section is intended to apply,
389 | and the section as a whole is intended to apply in other circumstances.
390 |
391 | It is not the purpose of this section to induce you to infringe any
392 | patents or other property right claims or to contest validity of any
393 | such claims; this section has the sole purpose of protecting the
394 | integrity of the free software distribution system which is
395 | implemented by public license practices. Many people have made
396 | generous contributions to the wide range of software distributed
397 | through that system in reliance on consistent application of that
398 | system; it is up to the author/donor to decide if he or she is willing
399 | to distribute software through any other system and a licensee cannot
400 | impose that choice.
401 |
402 | This section is intended to make thoroughly clear what is believed to
403 | be a consequence of the rest of this License.
404 |
405 | 12. If the distribution and/or use of the Library is restricted in
406 | certain countries either by patents or by copyrighted interfaces, the
407 | original copyright holder who places the Library under this License may add
408 | an explicit geographical distribution limitation excluding those countries,
409 | so that distribution is permitted only in or among countries not thus
410 | excluded. In such case, this License incorporates the limitation as if
411 | written in the body of this License.
412 |
413 | 13. The Free Software Foundation may publish revised and/or new
414 | versions of the Lesser General Public License from time to time.
415 | Such new versions will be similar in spirit to the present version,
416 | but may differ in detail to address new problems or concerns.
417 |
418 | Each version is given a distinguishing version number. If the Library
419 | specifies a version number of this License which applies to it and
420 | "any later version", you have the option of following the terms and
421 | conditions either of that version or of any later version published by
422 | the Free Software Foundation. If the Library does not specify a
423 | license version number, you may choose any version ever published by
424 | the Free Software Foundation.
425 |
426 | 14. If you wish to incorporate parts of the Library into other free
427 | programs whose distribution conditions are incompatible with these,
428 | write to the author to ask for permission. For software which is
429 | copyrighted by the Free Software Foundation, write to the Free
430 | Software Foundation; we sometimes make exceptions for this. Our
431 | decision will be guided by the two goals of preserving the free status
432 | of all derivatives of our free software and of promoting the sharing
433 | and reuse of software generally.
434 |
435 | NO WARRANTY
436 |
437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
446 |
447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
456 | DAMAGES.
457 |
458 | END OF TERMS AND CONDITIONS
459 |
460 | How to Apply These Terms to Your New Libraries
461 |
462 | If you develop a new library, and you want it to be of the greatest
463 | possible use to the public, we recommend making it free software that
464 | everyone can redistribute and change. You can do so by permitting
465 | redistribution under these terms (or, alternatively, under the terms of the
466 | ordinary General Public License).
467 |
468 | To apply these terms, attach the following notices to the library. It is
469 | safest to attach them to the start of each source file to most effectively
470 | convey the exclusion of warranty; and each file should have at least the
471 | "copyright" line and a pointer to where the full notice is found.
472 |
473 |
474 | Copyright (C)
475 |
476 | This library is free software; you can redistribute it and/or
477 | modify it under the terms of the GNU Lesser General Public
478 | License as published by the Free Software Foundation; either
479 | version 2.1 of the License, or (at your option) any later version.
480 |
481 | This library is distributed in the hope that it will be useful,
482 | but WITHOUT ANY WARRANTY; without even the implied warranty of
483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
484 | Lesser General Public License for more details.
485 |
486 | You should have received a copy of the GNU Lesser General Public
487 | License along with this library; if not, write to the Free Software
488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
489 | USA
490 |
491 | Also add information on how to contact you by electronic and paper mail.
492 |
493 | You should also get your employer (if you work as a programmer) or your
494 | school, if any, to sign a "copyright disclaimer" for the library, if
495 | necessary. Here is a sample; alter the names:
496 |
497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the
498 | library `Frob' (a library for tweaking knobs) written by James Random
499 | Hacker.
500 |
501 | , 1 April 1990
502 | Ty Coon, President of Vice
503 |
504 | That's all there is to it!
505 |
--------------------------------------------------------------------------------