├── docs ├── .nojekyll ├── changelog.rst ├── locale │ └── zh_CN │ │ └── LC_MESSAGES │ │ ├── index.mo │ │ ├── README.mo │ │ ├── api │ │ ├── error.mo │ │ ├── dftt_timecode.mo │ │ ├── dftt_timerange.mo │ │ └── error.po │ │ ├── changelog.mo │ │ ├── quickstart.mo │ │ ├── user_guide.mo │ │ ├── contributing.mo │ │ ├── installation.mo │ │ ├── quickstart.po │ │ ├── index.po │ │ ├── installation.po │ │ ├── README.po │ │ ├── user_guide.po │ │ ├── changelog.po │ │ └── contributing.po ├── _static │ └── switcher.json ├── .gitignore ├── make.bat ├── _templates │ └── language-switcher.html ├── api │ ├── dftt_timerange.rst │ ├── dftt_timecode.rst │ └── error.rst ├── README.md ├── Makefile ├── installation.rst ├── index.rst ├── user_guide.rst ├── conf.py ├── quickstart.rst └── contributing.rst ├── test ├── __init__.py ├── test_logging_config.py └── test_dftt_timerange.py ├── .gitignore ├── setup.py ├── .github └── workflows │ ├── publish-to-pypi.yml │ └── docs.yml ├── pyproject.toml ├── dftt_timecode ├── error.py ├── pattern.py ├── __init__.py └── logging_config.py ├── README.md ├── CHANGELOG.md └── LICENSE /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.md 2 | :parser: myst_parser.sphinx_ 3 | -------------------------------------------------------------------------------- /docs/locale/zh_CN/LC_MESSAGES/index.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwenYou/dftt_timecode/HEAD/docs/locale/zh_CN/LC_MESSAGES/index.mo -------------------------------------------------------------------------------- /docs/locale/zh_CN/LC_MESSAGES/README.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwenYou/dftt_timecode/HEAD/docs/locale/zh_CN/LC_MESSAGES/README.mo -------------------------------------------------------------------------------- /docs/locale/zh_CN/LC_MESSAGES/api/error.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwenYou/dftt_timecode/HEAD/docs/locale/zh_CN/LC_MESSAGES/api/error.mo -------------------------------------------------------------------------------- /docs/locale/zh_CN/LC_MESSAGES/changelog.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwenYou/dftt_timecode/HEAD/docs/locale/zh_CN/LC_MESSAGES/changelog.mo -------------------------------------------------------------------------------- /docs/locale/zh_CN/LC_MESSAGES/quickstart.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwenYou/dftt_timecode/HEAD/docs/locale/zh_CN/LC_MESSAGES/quickstart.mo -------------------------------------------------------------------------------- /docs/locale/zh_CN/LC_MESSAGES/user_guide.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwenYou/dftt_timecode/HEAD/docs/locale/zh_CN/LC_MESSAGES/user_guide.mo -------------------------------------------------------------------------------- /docs/locale/zh_CN/LC_MESSAGES/contributing.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwenYou/dftt_timecode/HEAD/docs/locale/zh_CN/LC_MESSAGES/contributing.mo -------------------------------------------------------------------------------- /docs/locale/zh_CN/LC_MESSAGES/installation.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwenYou/dftt_timecode/HEAD/docs/locale/zh_CN/LC_MESSAGES/installation.mo -------------------------------------------------------------------------------- /docs/locale/zh_CN/LC_MESSAGES/api/dftt_timecode.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwenYou/dftt_timecode/HEAD/docs/locale/zh_CN/LC_MESSAGES/api/dftt_timecode.mo -------------------------------------------------------------------------------- /docs/locale/zh_CN/LC_MESSAGES/api/dftt_timerange.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwenYou/dftt_timecode/HEAD/docs/locale/zh_CN/LC_MESSAGES/api/dftt_timerange.mo -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dftt_timecode/__pycache__ 2 | .vscode 3 | __pycache__ 4 | .venv 5 | .DS_Store 6 | .env 7 | dftt_timecode.egg-info 8 | .pytest_cache 9 | .DS_Store 10 | playground.ipynb 11 | CLAUDE.md 12 | dist 13 | build 14 | .claude/settings.local.json 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup script for dftt_timecode. 3 | 4 | This file is maintained for backward compatibility. 5 | The main configuration is in pyproject.toml. 6 | """ 7 | import setuptools 8 | 9 | if __name__ == "__main__": 10 | setuptools.setup() 11 | -------------------------------------------------------------------------------- /docs/_static/switcher.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "English", 4 | "version": "en", 5 | "url": "https://owenyou.github.io/dftt_timecode/" 6 | }, 7 | { 8 | "name": "中文 (简体)", 9 | "version": "zh_CN", 10 | "url": "https://owenyou.github.io/dftt_timecode/zh_CN/" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Sphinx build outputs 2 | _build/ 3 | 4 | # Generated files 5 | *.pyc 6 | __pycache__/ 7 | .DS_Store 8 | Thumbs.db 9 | 10 | # Note: _static/ and _templates/ are NOT ignored 11 | # because they contain source files for the documentation 12 | # (custom templates and static assets like switcher.json) 13 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Test and Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Set up Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.11" 17 | 18 | - name: Install uv 19 | uses: astral-sh/setup-uv@v5 20 | 21 | - name: Build package 22 | run: uv build 23 | 24 | - name: Publish to PyPI 25 | uses: pypa/gh-action-pypi-publish@release/v1 26 | with: 27 | user: __token__ 28 | password: ${{ secrets.PYPI_API_TOKEN }} 29 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/_templates/language-switcher.html: -------------------------------------------------------------------------------- 1 | {# Language switcher dropdown for the navigation bar #} 2 | 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 | [![pypi](https://img.shields.io/badge/pypi-0.0.14-brightgreen)](https://pypi.org/project/dftt-timecode/) 4 | [![python](https://img.shields.io/badge/python-3.11+-blue)](https://www.python.org/) 5 | [![GitHub license](https://img.shields.io/badge/license-LGPL2.1-green)](https://github.com/OwenYou/dftt_timecode/blob/main/LICENSE) 6 | [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](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 | --------------------------------------------------------------------------------