├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.md ├── requirements-dev.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── test_timecode.py ├── timecode ├── __init__.py └── py.typed ├── upload_to_pypi └── upload_to_pypi.cmd /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master, develop ] 9 | pull_request: 10 | branches: [ master, develop ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install flake8 pytest 30 | if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Test with pytest 38 | run: | 39 | pytest 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~* 2 | *.pyc 3 | .DS_Store 4 | .coverage 5 | *.egg-info/* 6 | *.swp 7 | .venv/* 8 | docs/html/* 9 | docs/latex/* 10 | docs/doctrees/* 11 | docs/source/generated/* 12 | dist/ 13 | build/* 14 | include/* 15 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Changes 3 | ======= 4 | 5 | 1.3.1 6 | ===== 7 | 8 | * **Fix:** Fixed 23.98, 29.97 DF, 29.97 NDF, 59.94 and 59.94 NDF rollover to ``00:00:00:00`` after 24 hours. 9 | 10 | 1.3.0 11 | ===== 12 | 13 | * **Fix:** Fixed a huge bug in 29.97 NDF and 59.97 NDF calculations introduced 14 | in v1.2.3. 15 | 16 | * **Fix:** Fixed ``Timecode.framerate`` when it is given as ``23.98``. The 17 | ``framerate`` attribute will not be forced to ``24`` and it will stay 18 | ``23.98``. 19 | 20 | * **Update:** ``Timecode.tc_to_frames()`` method now accepts Timecode instances 21 | with possibly different frame rates then the instance itself. 22 | 23 | * **Fix:** Fixed ``Timecode.div_frames()`` method. 24 | 25 | * **Update:** Test coverage has been increased to 100% (yay!) 26 | 27 | 1.2.5 28 | ===== 29 | 30 | * **Fix:** Fixed an edge case when two Timecodes are subtracted the resultant 31 | Timecode will always have the correct amount of frames. But it is not 32 | possible to have a Timecode with negative or zero frames as this is changed 33 | in 1.2.3. 34 | 35 | * **Fix:** Fixed ``Timecode.float`` property for drop frames. 36 | 37 | 1.2.4 38 | ===== 39 | 40 | * **Update:** It is now possible to supply a ``Fraction`` instances for the 41 | ``framerate`` argument. 42 | 43 | 1.2.3 44 | ===== 45 | * **Update:** Passing ``frames=0`` will now raise a ValueError. This hopefully 46 | will clarify the usage of the TimeCode as a duration. If there is no 47 | duration, hence the ``frames=0``, meaning that the number of frames of the 48 | duration that this TimeCode represents is 0, which is meaningless. 49 | * **Update:** Also added some validation for the ``frames`` property 50 | (oh yes it is a property now). 51 | 52 | 1.2.2.1 53 | ======= 54 | * **Fix:** Fixed the ``CHANGELOG.rst`` and ``setup.py`` to be able to properly 55 | package and upload to PyPI. 56 | 57 | 1.2.2 58 | ===== 59 | * **Fix:** Fixed ``Timecode.parse_timecode`` for int inputs. 60 | * **Update:** ``Timecode`` now accepts a ``fractional`` bool argument that 61 | forces the timecode to be fractional. 62 | * **Update:** ``Timecode`` now accepts a ```force_non_drop_frame`` argument 63 | that forces the timecode to be non-drop frame. 64 | 65 | 1.2.1 66 | ===== 67 | * **Update:** Added support for 23.976 fps which is a common variation of 23.98. 68 | 69 | 1.2.0 70 | ===== 71 | * **NEW:** Support for passing a tuple with numerator and denominator when 72 | passing rational framerate. 73 | 74 | * **NEW:** set_fractional method for setting whether or not to represent a 75 | timecode as fractional seconds. 76 | 77 | * **Update:** Updated README's with info on new features 78 | 79 | * **FIX:** Some merge issues. 80 | 81 | 1.1.0 82 | ===== 83 | 84 | * **New:** Support for passing "binary coded decimal" (BCD) integer to 85 | timecode argument as it's stored in certain formats like OpenEXR and DPX. 86 | Useful for parsing timecode from metadata through OpenImageIO for instance. 87 | Example: ``Timecode(24, 421729315) -> 19:23:14:23`` 88 | https://en.wikipedia.org/wiki/SMPTE_timecode 89 | 90 | 1.0.1 91 | ===== 92 | 93 | * **Update:** To prevent confusion, passing 0 for ``start_seconds`` argument 94 | will raise a ValueError now in ``Timecode.__init__`` method. 95 | 96 | 1.0.0 97 | ===== 98 | 99 | * **New:** Added support for passing rational frame rate. 24000/1001 for 23.97 100 | etc. 101 | 102 | * **New:** Added tests for new functionality. Fractional seconds and 103 | rational frame rates. 104 | 105 | * **New:** added __ge__ and __le__ methods for better comparison between two 106 | timecodes. 107 | 108 | * **New:** Added support for fractional seconds in the frame field as used in 109 | ffmpeg's duration for instance. 110 | 111 | * **Important:** When passing fractional second style timecode, the 112 | Timecode.frs will return a float representing the fraction of a second. This 113 | is a major change for people expecting int values 114 | 115 | 0.4.2 116 | ===== 117 | 118 | * **Update:** Version bump for PyPI. 119 | 120 | 0.4.1 121 | ===== 122 | 123 | * **Fix:** Fixed a test that was testing overloaded operators. 124 | 125 | 0.4.0 126 | ===== 127 | 128 | * **New:** Frame delimiter is now set to ":" for Non Drop Frame, ";" for Drop 129 | Frame and "." for millisecond based time codes. 130 | If ``Timecode.__init__()`` start_timecode is passed a string with the wrong 131 | delimiter it will be converted automatically. 132 | 133 | * **Update:** All tests involving Drop Frame and millisecond time codes are now 134 | set to use the new delimiter. 135 | 136 | * **New:** ``Timecode.tc_to_string()`` method added to present the correctly 137 | formatted time code. 138 | 139 | * **New:** ``Timecode.ms_frame`` boolean attribute added. 140 | 141 | * **New:** ``Timecode.__init__()`` now supports strings, ints and floats for 142 | the framerate argument. 143 | 144 | 0.3.0 145 | ===== 146 | 147 | * **New:** Renamed the library to ``timecode``. 148 | 149 | 0.2.0 150 | ===== 151 | 152 | * **New:** Rewritten the whole library from scratch. 153 | 154 | * **New:** Most important change is the licencing. There was now license 155 | defined in the previous implementation. The library is now licensed under MIT 156 | license. 157 | 158 | * **Update:** Timecode.__init__() arguments has been changed, removed the 159 | unnecessary ``drop_frame``, ``iter_returns`` arguments. 160 | 161 | Drop frame can be interpreted from the ``framerate`` argument and 162 | ``iter_returns`` is unnecessary cause any iteration on the object will return 163 | another ``Timecode`` instance. 164 | 165 | If you want to get a string representation use ``Timecode.__str__()`` or 166 | ``str(Timecode)`` or ``Timecode.__repr__()`` or ``\`Timecode\``` or 167 | ``'%s' % Timecode`` any other thing that will convert it to a string. 168 | 169 | If you want to get an integer use ``Timecode.frames`` or 170 | ``Timecode.frame_count`` depending on what you want to get out of it. 171 | 172 | So setting the ``iter_returns`` to something and nailing the output was 173 | unnecessary. 174 | 175 | * **Update:** Updated the drop frame calculation to a much better one, which 176 | is based on to the blog post of David Heidelberger at 177 | http://www.davidheidelberger.com/blog/?p=29 178 | 179 | * **New:** Added ``Timecode.__eq__()`` so it is now possible to check the 180 | equality of two timecode instances or a timecode and a string or a timecode 181 | and an integer (which will check the total frame count). 182 | 183 | * **Update:** ``Timecode.tc_to_frames()`` now needs a timecode as a string 184 | and will return an integer value which is the number of frames in that 185 | timecode. 186 | 187 | * **Update:** ``Timecode.frames_to_tc()`` now needs an integer frame count 188 | and returns 4 integers for hours, minutes, seconds and frames. 189 | 190 | * **Update:** ``Timecode.hrs``, ``Timecode.mins``, ``Timecode.secs`` and 191 | ``Timecode.frs`` attributes are now properties. Because it was so rare to 192 | check the individual hours, minutes, seconds or frame values, their values 193 | are calculated with ``Timecode.frames_to_tc()`` method. But in future they 194 | can still be converted to attributes and their value will be updated each 195 | time the ``Timecode.frames`` attribute is changed (so add a ``_frames`` 196 | attribute and make ``frames`` a property with a getter and setter, and update 197 | the hrs, mins, secs and frs in setter etc.). 198 | 199 | * **Update:** Removed ``Timecode.calc_drop_frame()`` method. The drop frame 200 | calculation is neatly done inside ``Timecode.frames_to_tc()`` and 201 | ``Timecode.tc_to_frames()`` methods. 202 | 203 | * **Update:** Updated ``Timecode.parse_timecode()`` method to a much simpler 204 | algorithm. 205 | 206 | * **Update:** Removed ``Timecode.__return_item__()`` method. It is not 207 | necessary to return an item in that way anymore. 208 | 209 | * **Update:** Removed ``Timecode.make_timecode()`` method. It was another 210 | unnecessary method, so it is removed. Now using simple python string 211 | templates for string representations. 212 | 213 | * **New:** Added ``timecode.__version__`` string, and set the value to 214 | "0.2.0". 215 | 216 | * **Update:** Removed ``Timecode.set_int_framerate()`` method. Setting the 217 | framerate will automatically set the ``Timecode.int_framerate`` attribute. 218 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Joshua Banton and PyTimeCode developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.ini *.cfg *.rst *.py 2 | 3 | include CHANGELOG 4 | include TODO 5 | include README 6 | include INSTALL 7 | include MANIFEST.in 8 | include LICENSE 9 | include timecode.py 10 | recursive-include docs * 11 | recursive-include tests * 12 | 13 | prune docs/build 14 | prune docs/source/generated 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | About 2 | ----- 3 | 4 | Python module for manipulating SMPTE timecode. Supports any arbitrary integer frame 5 | rates and some default str values of 23.976, 23.98, 24, 25, 29.97, 30, 50, 59.94, 60 6 | frame rates and milliseconds (1000 fps) and fractional frame rates like "30001/1001". 7 | 8 | This library is a fork of the original PyTimeCode python library. You should not use 9 | the two library together (PyTimeCode is not maintained and has known bugs). 10 | 11 | The math behind the drop frame calculation is based on the 12 | [blog post of David Heidelberger](http://www.davidheidelberger.com/blog/?p=29). 13 | 14 | Simple math operations like, addition, subtraction, multiplication or division with an 15 | integer value or with a timecode is possible. Math operations between timecodes with 16 | different frame rates are supported. So: 17 | 18 | ```py 19 | from timecode import Timecode 20 | 21 | tc1 = Timecode('29.97', '00:00:00;00') 22 | tc2 = Timecode(24, '00:00:00:10') 23 | tc3 = tc1 + tc2 24 | assert tc3.framerate == '29.97' 25 | assert tc3.frames == 12 26 | assert tc3 == '00:00:00:11' 27 | ``` 28 | 29 | Creating a Timecode instance with a start timecode of '00:00:00:00' will result a 30 | timecode object where the total number of frames is 1. So: 31 | 32 | ```py 33 | tc4 = Timecode('24', '00:00:00:00') 34 | assert tc4.frames == 1 35 | ``` 36 | 37 | Use the `frame_number` attribute if you want to get a 0 based frame number: 38 | 39 | ```py 40 | assert tc4.frame_number == 0 41 | ``` 42 | 43 | > [!NOTE] 44 | > A common misconception is that `00:00:00:00` should have 0 frames. This is wrong 45 | > because Timecode is a label given for each frame in a media, and it happens to be 46 | > using numbers which are seemingly incremented one after another. So, for a Timecode to 47 | > exist there should be a frame. and 00:00:00:00 is generally the label given to the 48 | > first frame. 49 | 50 | Frame rates 29.97 and 59.94 are always drop frame, and all the others are non drop 51 | frame. 52 | 53 | The timecode library supports fractional frame rates passed as a string: 54 | 55 | ```py 56 | tc5 = Timecode('30000/1001', '00:00:00;00') 57 | assert tc5.framerate == '29.97' 58 | ``` 59 | 60 | You may also pass a big "Binary Coded Decimal" integer as start timecode: 61 | 62 | ```py 63 | tc6 = Timecode('24', 421729315) 64 | assert repr(tc6) == '19:23:14:23' 65 | ``` 66 | 67 | This is useful for parsing timecodes stored in OpenEXR's and extracted through 68 | OpenImageIO for instance. 69 | 70 | Timecode also supports passing start timecodes formatted like HH:MM:SS.sss where SS.sss 71 | is seconds and fractions of seconds: 72 | 73 | ```py 74 | tc8 = Timecode(25, '00:00:00.040') 75 | assert tc8.frame_number == 1 76 | ``` 77 | 78 | You may set any timecode to be represented as fractions of seconds: 79 | 80 | ```py 81 | tc9 = Timecode(24, '19:23:14:23') 82 | assert repr(tc9) == '19:23:14:23' 83 | 84 | tc9.set_fractional(True) 85 | assert repr(tc9) == '19:23:14.958' 86 | ``` 87 | 88 | Fraction of seconds is useful when working with tools like FFmpeg. 89 | 90 | The SMPTE standard limits the timecode with 24 hours. Even though, Timecode instance 91 | will show the current timecode inline with the SMPTE standard, it will keep counting the 92 | total frames without clipping it. 93 | 94 | Please report any bugs to the [GitHub](https://github.com/eoyilmaz/timecode) page. 95 | 96 | Copyright 2014 Joshua Banton and PyTimeCode developers. 97 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | build 2 | mock; python_version == '2.7' 3 | pytest 4 | pytest-cov 5 | pytest-xdist 6 | twine -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | match=^test 3 | nocapture=1 4 | cover-package=timecode 5 | with-coverage=1 6 | cover-erase=1 7 | 8 | [compile_catalog] 9 | directory = timecode/locale 10 | domain = timecode 11 | statistics = true 12 | 13 | [extract_messages] 14 | add_comments = TRANSLATORS: 15 | output_file = timecode/locale/timecode.pot 16 | width = 80 17 | 18 | [init_catalog] 19 | domain = timecode 20 | input_file = timecode/locale/timecode.pot 21 | output_dir = timecode/locale 22 | 23 | [update_catalog] 24 | domain = timecode 25 | input_file = timecode/locale/timecode.pot 26 | output_dir = timecode/locale 27 | previous = true 28 | 29 | [bdist_wheel] 30 | universal=1 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!-*- coding: utf-8 -*- 2 | 3 | import re 4 | import os 5 | from setuptools import setup, find_packages 6 | 7 | 8 | def read_file(file_path): 9 | """Read the given file at file_path. 10 | 11 | Args: 12 | file_path (str): The file path to read. 13 | 14 | Returns: 15 | str: The file content. 16 | """ 17 | with open(file_path, encoding="utf-8") as f: 18 | data = f.read() 19 | return data 20 | 21 | 22 | here = os.path.abspath(os.path.dirname(__file__)) 23 | README = read_file(os.path.join(here, 'README.rst')) 24 | CHANGES = read_file(os.path.join(here, 'CHANGELOG.rst')) 25 | METADATA = read_file(os.path.join(here, "timecode", "__init__.py")) 26 | 27 | 28 | def get_meta(meta): 29 | """Return meta. 30 | 31 | Args: 32 | meta (str): The meta to read. i.e. ``version``, ``author`` etc. 33 | """ 34 | meta_match = re.search( 35 | r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), 36 | METADATA, re.M 37 | ) 38 | if meta_match: 39 | meta_value = meta_match.group(1) 40 | return meta_value 41 | 42 | 43 | setup( 44 | name=get_meta("name"), 45 | version=get_meta("version"), 46 | description=get_meta("description"), 47 | long_description='%s\n\n%s' % (README, CHANGES), 48 | long_description_content_type='text/x-rst', 49 | classifiers=[ 50 | "Programming Language :: Python", 51 | "License :: OSI Approved :: MIT License", 52 | "Operating System :: OS Independent", 53 | "Development Status :: 5 - Production/Stable", 54 | "Topic :: Software Development :: Libraries :: Python Modules", 55 | ], 56 | author=get_meta("author"), 57 | author_email=get_meta("author_email"), 58 | url=get_meta("url"), 59 | keywords=['video', 'timecode', 'smpte'], 60 | packages=find_packages(), 61 | include_package_data=True, 62 | package_data={ 63 | "timecode": ["py.typed"], 64 | }, 65 | python_requires=">=3.7", 66 | zip_safe=True, 67 | ) 68 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/timecode/224088b48352479201d4c4c6cc4711556dea3bf4/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_timecode.py: -------------------------------------------------------------------------------- 1 | #!-*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from timecode import Timecode, TimecodeError 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "args,kwargs", [ 9 | [["12", "00:00:00:00"], {}], 10 | [["23.976", "00:00:00:00"], {}], 11 | [["23.98", "00:00:00:00"], {}], 12 | [["24", "00:00:00:00"], {}], 13 | [["25", "00:00:00:00"], {}], 14 | [["29.97", "00:00:00;00"], {}], 15 | [["30", "00:00:00:00"], {}], 16 | [["50", "00:00:00:00"], {}], 17 | [["59.94", "00:00:00;00"], {}], 18 | [["60", "00:00:00:00"], {}], 19 | [["ms", "03:36:09.230"], {}], 20 | [["24"], {"start_timecode": None, "frames": 12000}], 21 | [["23.976"], {}], 22 | [["23.98"], {}], 23 | [["24"], {}], 24 | [["25"], {}], 25 | [["29.97"], {}], 26 | [["30"], {}], 27 | [["50"], {}], 28 | [["59.94"], {}], 29 | [["60"], {}], 30 | [["ms"], {}], 31 | [["23.976", 421729315], {}], 32 | [["23.98", 421729315], {}], 33 | [["24", 421729315], {}], 34 | [["25", 421729315], {}], 35 | [["29.97", 421729315], {}], 36 | [["30", 421729315], {}], 37 | [["50", 421729315], {}], 38 | [["59.94", 421729315], {}], 39 | [["60", 421729315], {}], 40 | [["ms", 421729315], {}], 41 | [["24000/1000", "00:00:00:00"], {}], 42 | [["24000/1001", "00:00:00;00"], {}], 43 | [["30000/1000", "00:00:00:00"], {}], 44 | [["30000/1001", "00:00:00;00"], {}], 45 | [["60000/1000", "00:00:00:00"], {}], 46 | [["60000/1001", "00:00:00;00"], {}], 47 | [[(24000, 1000), "00:00:00:00"], {}], 48 | [[(24000, 1001), "00:00:00;00"], {}], 49 | [[(30000, 1000), "00:00:00:00"], {}], 50 | [[(30000, 1001), "00:00:00;00"], {}], 51 | [[(60000, 1000), "00:00:00:00"], {}], 52 | [[(60000, 1001), "00:00:00;00"], {}], 53 | [[12], {"frames": 12000}], 54 | [[24], {"frames": 12000}], 55 | [[23.976, "00:00:00:00"], {}], 56 | [[23.98, "00:00:00:00"], {}], 57 | [[24, "00:00:00:00"], {}], 58 | [[25, "00:00:00:00"], {}], 59 | [[29.97, "00:00:00;00"], {}], 60 | [[30, "00:00:00:00"], {}], 61 | [[50, "00:00:00:00"], {}], 62 | [[59.94, "00:00:00;00"], {}], 63 | [[60, "00:00:00:00"], {}], 64 | [[1000, "03:36:09.230"], {}], 65 | [[24], {"start_timecode": None, "frames": 12000}], 66 | [[23.976], {}], 67 | [[23.98], {}], 68 | [[24], {}], 69 | [[25], {}], 70 | [[29.97], {}], 71 | [[30], {}], 72 | [[50], {}], 73 | [[60], {}], 74 | [[1000], {}], 75 | [[24], {"frames": 12000}], 76 | ] 77 | ) 78 | def test_instance_creation(args, kwargs): 79 | """Instance creation, none of these should raise any error.""" 80 | tc = Timecode(*args, **kwargs) 81 | assert isinstance(tc, Timecode) 82 | 83 | 84 | def test_2398_vs_23976(): 85 | """Test 23.98 vs 23.976 fps.""" 86 | tc1 = Timecode("23.98", "04:01:45:23") 87 | tc2 = Timecode("23.976", "04:01:45:23") 88 | assert tc1._frames == tc2._frames 89 | assert repr(tc1) == repr(tc2) 90 | 91 | 92 | @pytest.mark.parametrize( 93 | "args,kwargs,expected_result,operator", [ 94 | [["24", "01:00:00:00"], {}, "01:00:00:00", True], 95 | [["23.98", "20:00:00:00"], {}, "20:00:00:00", True], 96 | [["29.97", "00:09:00;00"], {}, "00:08:59;28", True], 97 | [["29.97", "00:09:00:00"], {"force_non_drop_frame": True}, "00:09:00:00", True], 98 | [["30", "00:10:00:00"], {}, "00:10:00:00", True], 99 | [["60", "00:00:09:00"], {}, "00:00:09:00", True], 100 | [["59.94", "00:00:20;00"], {}, "00:00:20;00", True], 101 | [["59.94", "00:00:20;00"], {}, "00:00:20:00", False], 102 | [["ms", "00:00:00.900"], {}, "00:00:00.900", True], 103 | [["ms", "00:00:00.900"], {}, "00:00:00:900", False], 104 | [["24"], {"frames": 49}, "00:00:02:00", True], 105 | [["59.94", "00:09:00:00"], {"force_non_drop_frame": True}, "00:09:00:00", True], 106 | [["59.94", "04:20:13;21"], {}, "04:20:13;21", True], 107 | [["59.94"], {"frames": 935866}, "04:20:13;21", True], 108 | ] 109 | ) 110 | def test_repr_overload(args, kwargs, expected_result, operator): 111 | """Several timecode initialization.""" 112 | tc = Timecode(*args, **kwargs) 113 | if operator: 114 | assert expected_result == tc.__repr__() 115 | else: 116 | assert expected_result != tc.__repr__() 117 | 118 | 119 | def test_repr_overload_2(): 120 | """Several timecode initialization.""" 121 | tc1 = Timecode("59.94", frames=32401, force_non_drop_frame=True) 122 | tc2 = Timecode("59.94", "00:09:00:00", force_non_drop_frame=True) 123 | assert tc1 == tc2 124 | 125 | 126 | @pytest.mark.parametrize( 127 | "args,kwargs,expected_repr,expected_frames,is_drop_frame", [ 128 | [["29.97"], {}, "00:00:00;00", 1, None], 129 | [["29.97"], {"force_non_drop_frame": True}, "00:00:00:00", 1, None], 130 | [["29.97", "00:00:00;01"], {"force_non_drop_frame": True}, None, 2, None], 131 | [["29.97", "00:00:00:01"], {"force_non_drop_frame": True}, None, 2, None], 132 | [["29.97", "03:36:09;23"], {"force_non_drop_frame": False}, None, 388704, None], 133 | [["29.97", "03:36:09:23"], {"force_non_drop_frame": True}, None, 389094, None], 134 | [["29.97", "03:36:09;23"], {}, None, 388704, None], 135 | [["30", "03:36:09:23"], {}, None, 389094, None], 136 | [["25", "03:36:09:23"], {}, None, 324249, None], 137 | [["59.94", "03:36:09;23"], {}, None, 777384, None], 138 | [["60", "03:36:09:23"], {}, None, 778164, None], 139 | [["59.94", "03:36:09;23"], {}, None, 777384, None], 140 | [["23.98", "03:36:09:23"], {}, None, 311280, None], 141 | [["24", "03:36:09:23"], {}, None, 311280, None], 142 | [["24"], {"frames": 12000}, "00:08:19:23", None, None], 143 | [["25", 421729315], {}, "19:23:14:23", None, None], 144 | [["29.97", 421729315], {}, "19:23:14;23", None, True], 145 | [["23.98"], {"frames": 311280 * 720}, "01:59:59:23", None, None], 146 | [["23.98"], {"frames": 172800}, "01:59:59:23", None, None], 147 | ] 148 | ) 149 | def test_timecode_str_repr_tests(args, kwargs, expected_repr, expected_frames, is_drop_frame): 150 | """Several timecode initialization.""" 151 | tc = Timecode(*args, **kwargs) 152 | if expected_repr is not None: 153 | assert expected_repr == tc.__str__() 154 | if expected_frames is not None: 155 | assert expected_frames == tc._frames 156 | if is_drop_frame is not None: 157 | if is_drop_frame is True: 158 | assert tc.drop_frame is True 159 | else: 160 | assert tc.drop_frame is False 161 | 162 | 163 | def test_start_seconds_argument_is_zero(): 164 | """ValueError is raised if the start_seconds parameters is zero.""" 165 | with pytest.raises(ValueError) as cm: 166 | Timecode("29.97", start_seconds=0) 167 | 168 | assert str(cm.value) == "``start_seconds`` argument can not be 0" 169 | 170 | 171 | @pytest.mark.parametrize( 172 | "args,kwargs,hrs,mins,secs,frs,str_repr", [ 173 | [["ms", "03:36:09.230"], {}, 3, 36, 9, 230, None], 174 | [["29.97", "00:00:00;01"], {}, 0, 0, 0, 1, "00:00:00;01"], 175 | [["29.97", "03:36:09:23"], {}, 3, 36, 9, 23, None], 176 | [["29.97", "03:36:09;23"], {}, 3, 36, 9, 23, None], 177 | [["30", "03:36:09:23"], {}, 3, 36, 9, 23, None], 178 | [["25", "03:36:09:23"], {}, 3, 36, 9, 23, None], 179 | [["59.94", "03:36:09;23"], {}, 3, 36, 9, 23, None], 180 | [["60", "03:36:09:23"], {}, 3, 36, 9, 23, None], 181 | [["59.94", "03:36:09;23"], {}, 3, 36, 9, 23, None], 182 | [["23.98", "03:36:09:23"], {}, 3, 36, 9, 23, None], 183 | [["24", "03:36:09:23"], {}, 3, 36, 9, 23, None], 184 | [["ms", "03:36:09.230"], {}, 3, 36, 9, 230, None], 185 | [["24"], {"frames": 12000}, 0, 8, 19, 23, "00:08:19:23"], 186 | ] 187 | ) 188 | def test_timecode_properties_test(args, kwargs, hrs, mins, secs, frs, str_repr): 189 | """Test hrs, mins, secs and frs properties.""" 190 | tc = Timecode(*args, **kwargs) 191 | assert hrs == tc.hrs 192 | assert mins == tc.mins 193 | assert secs == tc.secs 194 | assert frs == tc.frs 195 | if str_repr is not None: 196 | assert str_repr == tc.__str__() 197 | 198 | 199 | @pytest.mark.parametrize( 200 | "args,kwargs,frames, str_repr, tc_next", [ 201 | [["29.97", "00:00:00;00"], {}, 1, None, None], 202 | [["29.97", "00:00:00;21"], {}, 22, None, None], 203 | [["29.97", "00:00:00;29"], {}, 30, None, None], 204 | [["29.97", "00:00:00;60"], {}, 61, None, None], 205 | [["29.97", "00:00:01;00"], {}, 31, None, None], 206 | [["29.97", "00:00:10;00"], {}, 301, None, None], 207 | [["29.97", "00:01:00;00"], {}, 1799, "00:00:59;28", None], 208 | [["29.97", "23:59:59;29"], {}, 2589408, None, None], 209 | [["29.97", "01:00:00;00"], {"force_non_drop_frame": True}, None, "01:00:00:00", None], 210 | [["29.97", "01:00:00:00"], {"force_non_drop_frame": True}, None, "01:00:00:00", None], 211 | [["29.97", "13:36:59;29"], {}, None, None, "13:37:00;02"], 212 | [["59.94", "13:36:59;59"], {}, None, "13:36:59;59", None], 213 | [["59.94", "13:36:59;59"], {}, None, None, "13:37:00;04"], 214 | [["59.94", "13:39:59;59"], {}, None, None, "13:40:00;00"], 215 | [["29.97", "13:39:59;29"], {}, None, None, "13:40:00;00"], 216 | ] 217 | ) 218 | def test_tc_to_frame_test_in_2997(args, kwargs, frames, str_repr, tc_next): 219 | """timecode to frame conversion is ok in 2997.""" 220 | tc = Timecode(*args, **kwargs) 221 | if frames is not None: 222 | assert frames == tc._frames 223 | if str_repr is not None: 224 | assert str_repr == tc.__str__() 225 | if tc_next is not None: 226 | assert tc_next == tc.next().__str__() 227 | 228 | 229 | def test_setting_frame_rate_to_2997_forces_drop_frame(): 230 | """Setting the frame rate to 29.97 forces the dropframe to True.""" 231 | tc = Timecode("29.97") 232 | assert tc.drop_frame 233 | 234 | 235 | def test_setting_frame_rate_to_5994_forces_drop_frame(): 236 | """Setting the frame rate to 59.94 forces the dropframe to True.""" 237 | tc = Timecode("59.94") 238 | assert tc.drop_frame 239 | 240 | 241 | def test_setting_frame_rate_to_ms_forces_drop_frame(): 242 | """Setting the frame rate to ms forces the ms_frame to True.""" 243 | tc = Timecode("ms") 244 | assert tc.ms_frame 245 | 246 | 247 | def test_setting_frame_rate_to_1000_forces_drop_frame(): 248 | """Setting the frame rate to 1000 forces the ms_frame to True.""" 249 | tc = Timecode("1000") 250 | assert tc.ms_frame 251 | 252 | 253 | def test_framerate_argument_is_frames(): 254 | """Setting the framerate arg to 'frames' will set the integer frame rate to 1.""" 255 | tc = Timecode("frames") 256 | assert tc.framerate == "frames" 257 | assert tc._int_framerate == 1 258 | 259 | 260 | @pytest.mark.parametrize( 261 | "args,kwargs,str_repr,next_range,last_tc_str_repr,frames", [ 262 | [["29.97", "03:36:09;23"], {}, "03:36:09;23", 60, "03:36:11;23", 388764], 263 | [["30", "03:36:09:23"], {}, "03:36:09;23", 60, "03:36:11:23", 389154], 264 | [["25", "03:36:09:23"], {}, "03:36:09;23", 60, "03:36:12:08", 324309], 265 | [["59.94", "03:36:09;23"], {}, "03:36:09;23", 60, "03:36:10;23", 777444], 266 | [["60", "03:36:09:23"], {}, "03:36:09:23", 60, "03:36:10:23", 778224], 267 | [["59.94", "03:36:09:23"], {}, "03:36:09;23", 60, "03:36:10:23", 777444], 268 | [["23.98", "03:36:09:23"], {}, "03:36:09:23", 60, "03:36:12:11", 311340], 269 | [["24", "03:36:09:23"], {}, "03:36:09:23", 60, "03:36:12:11", 311340], 270 | [["ms", "03:36:09.230"], {}, "03:36:09.230", 60, "03:36:09.290", 12969291], 271 | [["24"], {"frames": 12000}, "00:08:19:23", 60, "00:08:22:11", 12060], 272 | ] 273 | ) 274 | def test_iteration(args, kwargs, str_repr, next_range, last_tc_str_repr, frames): 275 | """Test iteration.""" 276 | tc = Timecode(*args, **kwargs) 277 | assert tc == str_repr 278 | 279 | last_tc = None 280 | for x in range(next_range): 281 | last_tc = tc.next() 282 | assert last_tc is not None 283 | 284 | assert last_tc_str_repr == last_tc 285 | assert frames == tc._frames 286 | 287 | 288 | @pytest.mark.parametrize( 289 | "args1,kwargs1,args2,kwargs2,custom_offset1,custom_offset2,str_repr1,str_repr2,frames1, frames2", [ 290 | [["29.97", "03:36:09;23"], {}, ["29.97", "00:00:29;23"], {}, 894, 894, "03:36:39;17", "03:36:39;17", 389598, 389598], 291 | [["30", "03:36:09:23"], {}, ["30", "00:00:29:23"], {}, 894, 894, "03:36:39:17", "03:36:39:17", 389988, 389988], 292 | [["25", "03:36:09:23"], {}, ["25", "00:00:29:23"], {}, 749, 749, "03:36:39:22", "03:36:39:22", 324998, 324998], 293 | [["59.94", "03:36:09;23"], {}, ["59.94", "00:00:29;23"], {}, 1764, 1764, "03:36:38;47", "03:36:38;47", 779148, 779148], 294 | [["60", "03:36:09:23"], {}, ["60", "00:00:29:23"], {}, 1764, 1764, "03:36:38:47", "03:36:38:47", 779928, 779928], 295 | [["59.94", "03:36:09;23"], {}, ["59.94", "00:00:29;23"], {}, 1764, 1764, "03:36:38;47", "03:36:38;47", 779148, 779148], 296 | [["23.98", "03:36:09:23"], {}, ["23.98", "00:00:29:23"], {}, 720, 720, "03:36:39:23", "03:36:39:23", 312000, 312000], 297 | [["ms", "03:36:09.230"], {}, ["ms", "01:06:09.230"], {}, 3969231, 720, "04:42:18.461", "03:36:09.950", 16938462, 12969951], 298 | [["ms", "03:36:09.230"], {}, ["ms", "01:06:09.230"], {}, 3969231, 720, "04:42:18.461", "03:36:09.950", 16938462, 12969951], 299 | [["24"], {"frames": 12000}, ["24"], {"frames": 485}, 485, 719, "00:08:40:04", "00:08:49:22", 12485, 12719], 300 | [["59.94", "04:20:13;21"], {}, ["59.94", "23:59:59;59"], {}, 5178816, 0, "04:20:13;21", "04:20:13;21", 6114682, 935866], 301 | ] 302 | ) 303 | def test_op_overloads_add(args1, kwargs1, args2, kwargs2, custom_offset1, custom_offset2, str_repr1, str_repr2, frames1, frames2): 304 | """Test + operator overload.""" 305 | tc = Timecode(*args1, **kwargs1) 306 | tc2 = Timecode(*args2, **kwargs2) 307 | assert custom_offset1 == tc2._frames 308 | d = tc + tc2 309 | f = tc + custom_offset2 310 | assert str_repr1 == d.__str__() 311 | assert frames1 == d._frames 312 | assert str_repr2 == f.__str__() 313 | assert frames2 == f._frames 314 | 315 | 316 | @pytest.mark.parametrize( 317 | "args1,kwargs1,args2,kwargs2,custom_offset1,custom_offset2,str_repr1,str_repr2,frames1, frames2", [ 318 | [["29.97", "03:36:09;23"], {}, ["29.97", "00:00:29;23"], {}, 894, 894, "03:35:39;27", "03:35:39;27", 387810, 387810], 319 | [["30", "03:36:09:23"], {}, ["30", "00:00:29:23"], {}, 894, 894, "03:35:39:29", "03:35:39:29", 388200, 388200], 320 | [["25", "03:36:09:23"], {}, ["25", "00:00:29:23"], {}, 749, 749, "03:35:39:24", "03:35:39:24", 323500, 323500], 321 | [["59.94", "03:36:09;23"], {}, ["59.94", "00:00:29;23"], {}, 1764, 1764, "03:35:39;55", "03:35:39;55", 775620, 775620], 322 | [["60", "03:36:09:23"], {}, ["60", "00:00:29:23"], {}, 1764, 1764, "03:35:39:59", "03:35:39:59", 776400, 776400], 323 | [["59.94", "03:36:09;23"], {}, ["59.94", "00:00:29;23"], {}, 1764, 1764, "03:35:39;55", "03:35:39;55", 775620, 775620], 324 | [["23.98", "03:36:09:23"], {}, ["23.98", "00:00:29:23"], {}, 720, 720, "03:35:39:23", "03:35:39:23", 310560, 310560], 325 | [["23.98", "03:36:09:23"], {}, ["23.98", "00:00:29:23"], {}, 720, 720, "03:35:39:23", "03:35:39:23", 310560, 310560], 326 | [["ms", "03:36:09.230"], {}, ["ms", "01:06:09.230"], {}, 3969231, 3969231, "02:29:59.999", "02:29:59.999", 9000000, 9000000], 327 | [["24"], {"frames": 12000}, ["24"], {"frames": 485}, 485, 485, "00:07:59:18", "00:07:59:18", 11515, 11515], 328 | ] 329 | ) 330 | def test_op_overloads_subtract(args1, kwargs1, args2, kwargs2, custom_offset1, custom_offset2, str_repr1, str_repr2, frames1, frames2): 331 | """Test - operator overload.""" 332 | tc = Timecode(*args1, **kwargs1) 333 | tc2 = Timecode(*args2, **kwargs2) 334 | assert custom_offset1 == tc2._frames 335 | d = tc - tc2 336 | f = tc - custom_offset2 337 | assert str_repr1 == d.__str__() 338 | assert str_repr2 == f.__str__() 339 | assert frames1 == d._frames 340 | assert frames2 == f._frames 341 | 342 | 343 | @pytest.mark.parametrize( 344 | "args1,kwargs1,args2,kwargs2,custom_offset1,custom_offset2,str_repr1,str_repr2,frames1, frames2", [ 345 | [["29.97", "00:00:09;23"], {}, ["29.97", "00:00:29;23"], {}, 894, 4, "02:26:09;29", "00:00:39;05", 262836, 1176], 346 | [["30", "03:36:09:23"], {}, ["30", "00:00:29:23"], {}, 894, 894, "04:50:01:05", "04:50:01:05", 347850036, 347850036], 347 | [["25", "03:36:09:23"], {}, ["25", "00:00:29:23"], {}, 749, 749, "10:28:20:00", "10:28:20:00", 242862501, 242862501], 348 | [["59.94", "03:36:09;23"], {}, ["59.94", "00:00:29;23"], {}, 1764, 1764, "18:59:27;35", "18:59:27;35", 1371305376, 1371305376], 349 | [["60", "03:36:09:23"], {}, ["60", "00:00:29:23"], {}, 1764, 1764, "19:00:21:35", "19:00:21:35", 1372681296, 1372681296], 350 | [["59.94", "03:36:09;23"], {}, ["59.94", "00:00:29;23"], {}, 1764, 1764, "18:59:27;35", "18:59:27;35", 1371305376, 1371305376], 351 | [["ms", "03:36:09.230"], {}, ["ms", "01:06:09.230"], {}, 3969231, 3969231, "17:22:11.360", "17:22:11.360", 51477873731361, 51477873731361], 352 | [["24"], {"frames": 12000}, ["24"], {"frames": 485}, 485, 485, "19:21:39:23", "19:21:39:23", 5820000, 5820000], 353 | [["24"], {"frames": 12000}, ["24"], {"frames": 485}, 485, 485, "19:21:39:23", "19:21:39:23", 5820000, 5820000], 354 | ] 355 | ) 356 | def test_op_overloads_mult(args1, kwargs1, args2, kwargs2, custom_offset1, custom_offset2, str_repr1, str_repr2, frames1, frames2): 357 | """Test * operator overload.""" 358 | tc = Timecode(*args1, **kwargs1) 359 | tc2 = Timecode(*args2, **kwargs2) 360 | assert custom_offset1 == tc2._frames 361 | d = tc * tc2 362 | f = tc * custom_offset2 363 | assert str_repr1 == d.__str__() 364 | assert str_repr2 == f.__str__() 365 | assert frames1 == d._frames 366 | assert frames2 == f._frames 367 | 368 | 369 | def test_op_overloads_mult_1(): 370 | """Two Timecode multiplied, the framerate of the result is the same of left side.""" 371 | tc1 = Timecode("23.98", "03:36:09:23") 372 | tc2 = Timecode("23.98", "00:00:29:23") 373 | tc3 = tc1 * tc2 374 | assert tc3.framerate == "23.98" 375 | 376 | 377 | def test_op_overloads_mult_2(): 378 | """Two Timecode multiplied, the framerate of the result is the same of left side.""" 379 | tc1 = Timecode("23.98", "03:36:09:23") 380 | assert tc1._frames == 311280 381 | tc2 = Timecode("23.98", "00:00:29:23") 382 | assert tc2._frames == 720 383 | tc3 = tc1 * tc2 384 | assert 224121600 == tc3._frames 385 | assert "01:59:59:23" == tc3.__str__() 386 | 387 | 388 | def test_op_overloads_mult_3(): 389 | """Timecode multiplied with integer.""" 390 | tc1 = Timecode("23.98", "03:36:09:23") 391 | tc4 = tc1 * 720 392 | assert 224121600 == tc4._frames 393 | assert "01:59:59:23" == tc4.__str__() 394 | 395 | 396 | def test_add_with_two_different_frame_rates(): 397 | """Added TCs with different framerate, result framerate is same with left side.""" 398 | tc1 = Timecode("29.97", "00:00:00;00") 399 | tc2 = Timecode("24", "00:00:00:10") 400 | tc3 = tc1 + tc2 401 | assert "29.97" == tc3.framerate 402 | assert 12 == tc3._frames 403 | assert tc3 == "00:00:00;11" 404 | 405 | 406 | @pytest.mark.parametrize( 407 | "args,kwargs,func,tc2", [ 408 | [["24", "00:00:01:00"], {}, lambda x, y: x + y, "not suitable"], 409 | [["24", "00:00:01:00"], {}, lambda x, y: x - y, "not suitable"], 410 | [["24", "00:00:01:00"], {}, lambda x, y: x * y, "not suitable"], 411 | [["24", "00:00:01:00"], {}, lambda x, y: x / y, "not suitable"], 412 | [["24", "00:00:01:00"], {}, lambda x, y: x / y, 32.4], 413 | ] 414 | ) 415 | def test_add_with_non_suitable_class_instance(args, kwargs, func, tc2): 416 | """TimecodeError is raised if the other class is not suitable for the operation.""" 417 | tc1 = Timecode(*args, **kwargs) 418 | with pytest.raises(TimecodeError) as cm: 419 | _ = func(tc1, tc2) 420 | 421 | assert str(cm.value) == "Type {} not supported for arithmetic.".format( 422 | tc2.__class__.__name__ 423 | ) 424 | 425 | 426 | def test_div_method_working_properly_1(): 427 | """__div__ method is working properly.""" 428 | tc1 = Timecode("24", frames=100) 429 | tc2 = Timecode("24", frames=10) 430 | tc3 = tc1 / tc2 431 | assert tc3.frames == 10 432 | assert tc3 == "00:00:00:09" 433 | 434 | 435 | def test_div_method_working_properly_2(): 436 | """__div__ method is working properly.""" 437 | tc1 = Timecode("24", "00:00:10:00") 438 | tc2 = tc1 / 10 439 | assert tc2 == "00:00:00:23" 440 | 441 | 442 | @pytest.mark.parametrize( 443 | "args,frames,frame_number", [ 444 | [["24", "00:00:00:00"], 1, 0], 445 | [["24", "00:00:01:00"], 25, 24], 446 | [["29.97", "00:01:00;00"], 1799, 1798], 447 | [["30", "00:01:00:00"], 1801, 1800], 448 | [["50", "00:01:00:00"], 3001, 3000], 449 | [["59.94", "00:01:00;00"], 3597, 3596], 450 | [["60", "00:01:00:00"], 3601, 3600], 451 | ] 452 | ) 453 | def test_frame_number_attribute_value_is_correctly_calculated(args, frames, frame_number): 454 | """Timecode.frame_number attribute is correctly calculated.""" 455 | tc1 = Timecode(*args) 456 | assert frames == tc1._frames 457 | assert frame_number == tc1.frame_number 458 | 459 | 460 | def test_24_hour_limit_in_24fps(): 461 | """timecode will loop back to 00:00:00:00 after 24 hours in 24 fps.""" 462 | tc1 = Timecode("24", "00:00:00:21") 463 | tc2 = Timecode("24", "23:59:59:23") 464 | assert "00:00:00:21" == (tc1 + tc2).__str__() 465 | assert "02:00:00:00" == (tc2 + 159840001).__str__() 466 | 467 | 468 | def test_24_hour_limit_in_2997fps(): 469 | """timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 470 | tc1 = Timecode("29.97", "00:00:00;21") 471 | assert tc1.drop_frame 472 | assert 22 == tc1._frames 473 | 474 | tc2 = Timecode("29.97", "23:59:59;29") 475 | assert tc2.drop_frame 476 | assert 2589408 == tc2._frames 477 | 478 | assert "00:00:00;21" == tc1.__repr__() 479 | assert "23:59:59;29" == tc2.__repr__() 480 | 481 | assert "00:00:00;21" == (tc1 + tc2).__str__() 482 | assert "02:00:00;00" == (tc2 + 215785).__str__() 483 | assert "02:00:00;00" == (tc2 + 215785 + 2589408).__str__() 484 | assert "02:00:00;00" == (tc2 + 215785 + 2589408 + 2589408).__str__() 485 | 486 | 487 | def test_24_hour_limit_1(): 488 | """Timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 489 | tc1 = Timecode("59.94", "23:59:59;29") 490 | assert 5178786 == tc1._frames 491 | 492 | 493 | def test_24_hour_limit_2(): 494 | """Timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 495 | tc1 = Timecode("29.97", "23:59:59;29") 496 | assert 2589408 == tc1._frames 497 | 498 | 499 | def test_24_hour_limit_3(): 500 | """Timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 501 | tc1 = Timecode("29.97", frames=2589408) 502 | assert "23:59:59;29" == tc1.__str__() 503 | 504 | 505 | def test_24_hour_limit_4(): 506 | """Timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 507 | tc1 = Timecode("29.97", "23:59:59;29") 508 | tc2 = tc1 + 1 509 | assert "00:00:00;00" == tc2.__str__() 510 | 511 | 512 | def test_24_hour_limit_5(): 513 | """Timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 514 | tc1 = Timecode("29.97", "23:59:59;29") 515 | tc2 = tc1 + 21 516 | assert "00:00:00;20" == tc2.__str__() 517 | 518 | 519 | def test_24_hour_limit_6(): 520 | """Timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 521 | tc1 = Timecode("29.97", "00:00:00;21") 522 | tc2 = Timecode("29.97", "23:59:59;29") 523 | tc3 = tc1 + tc2 524 | assert "00:00:00;21" == tc3.__str__() 525 | 526 | 527 | def test_24_hour_limit_7(): 528 | """Timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 529 | tc1 = Timecode("29.97", "04:20:13;21") 530 | assert 467944 == tc1._frames 531 | assert "04:20:13;21" == tc1.__str__() 532 | 533 | 534 | def test_24_hour_limit_8(): 535 | """Timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 536 | tc1 = Timecode("29.97", frames=467944) 537 | assert 467944 == tc1._frames 538 | assert "04:20:13;21" == tc1.__str__() 539 | 540 | 541 | def test_24_hour_limit_9(): 542 | """Timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 543 | tc1 = Timecode("29.97", "23:59:59;29") 544 | assert 2589408 == tc1._frames 545 | assert "23:59:59;29" == tc1.__str__() 546 | 547 | 548 | def test_24_hour_limit_10(): 549 | """Timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 550 | tc1 = Timecode("29.97", frames=2589408) 551 | assert 2589408 == tc1._frames 552 | assert "23:59:59;29" == tc1.__str__() 553 | 554 | 555 | def test_24_hour_limit_11(): 556 | """Timecode will loop back to 00:00:00:00 after 24 hours in 29.97 fps.""" 557 | tc1 = Timecode("29.97", frames=467944) 558 | tc2 = Timecode('29.97', '23:59:59;29') 559 | tc3 = tc1 + tc2 560 | assert "04:20:13;21" == tc3.__str__() 561 | 562 | 563 | def test_framerate_can_be_changed(): 564 | """Timecode is automatically updated if the framerate attribute is changed.""" 565 | tc1 = Timecode("25", frames=100) 566 | assert "00:00:03:24" == tc1.__str__() 567 | assert 100 == tc1._frames 568 | 569 | tc1.framerate = "12" 570 | assert "00:00:08:03" == tc1.__str__() 571 | assert 100 == tc1._frames 572 | 573 | 574 | @pytest.mark.parametrize( 575 | "args,kwargs,frame_rate,int_framerate", [ 576 | [["24000/1000", "00:00:00:00"], {}, "24", 24], 577 | [["24000/1001", "00:00:00;00"], {}, "23.98", 24], 578 | [["30000/1000", "00:00:00:00"], {}, "30", 30], 579 | [["30000/1001", "00:00:00;00"], {}, "29.97", 30], 580 | [["60000/1000", "00:00:00:00"], {}, "60", 60], 581 | [["60000/1001", "00:00:00;00"], {}, "59.94", 60], 582 | [[(60000, 1001), "00:00:00;00"], {}, "59.94", 60], 583 | ] 584 | ) 585 | def test_rational_framerate_conversion(args, kwargs, frame_rate, int_framerate): 586 | """Fractional framerate conversion.""" 587 | tc = Timecode(*args, **kwargs) 588 | assert frame_rate == tc.framerate 589 | assert int_framerate == tc._int_framerate 590 | 591 | 592 | def test_rational_frame_delimiter_1(): 593 | tc = Timecode("24000/1000", frames=1) 594 | assert ";" not in tc.__repr__() 595 | 596 | 597 | def test_rational_frame_delimiter_2(): 598 | tc = Timecode("24000/1001", frames=1) 599 | assert ";" not in tc.__repr__() 600 | 601 | 602 | def test_rational_frame_delimiter_3(): 603 | tc = Timecode("30000/1001", frames=1) 604 | assert ";" in tc.__repr__() 605 | 606 | 607 | def test_ms_vs_fraction_frames_1(): 608 | tc1 = Timecode("ms", "00:00:00.040") 609 | assert tc1.ms_frame 610 | assert not tc1.fraction_frame 611 | 612 | 613 | def test_ms_vs_fraction_frames_2(): 614 | tc2 = Timecode(24, "00:00:00.042") 615 | assert tc2.fraction_frame 616 | assert not tc2.ms_frame 617 | 618 | 619 | def test_ms_vs_fraction_frames_3(): 620 | tc1 = Timecode("ms", "00:00:00.040") 621 | tc2 = Timecode(24, "00:00:00.042") 622 | assert tc1 != tc2 623 | 624 | 625 | def test_ms_vs_fraction_frames_4(): 626 | tc1 = Timecode("ms", "00:00:00.040") 627 | tc2 = Timecode(24, "00:00:00.042") 628 | assert tc1.frame_number == 40 629 | assert tc2.frame_number == 1 630 | 631 | 632 | def test_toggle_fractional_frame_1(): 633 | tc = Timecode(24, 421729315) 634 | assert tc.__repr__() == "19:23:14:23" 635 | 636 | 637 | def test_toggle_fractional_frame_2(): 638 | tc = Timecode(24, 421729315) 639 | tc.set_fractional(True) 640 | assert tc.__repr__() == "19:23:14.958" 641 | 642 | 643 | def test_toggle_fractional_frame_3(): 644 | tc = Timecode(24, 421729315) 645 | tc.set_fractional(False) 646 | assert tc.__repr__() == "19:23:14:23" 647 | 648 | 649 | def test_timestamp_realtime_1(): 650 | frames = 12345 651 | ts = frames*1/24 652 | assert Timecode(24, frames=frames).to_realtime(True) == ts 653 | 654 | 655 | def test_timestamp_realtime_2(): 656 | tc = Timecode(50, start_seconds=1/50) 657 | assert tc.to_realtime() == '00:00:00.020' 658 | 659 | 660 | def test_timestamp_realtime_3(): 661 | #SMPTE 12-1 §5.2.2: 662 | #- "When DF compensation is applied to NTSC TC, the deviation after one hour is approximately –3.6 ms" 663 | tc = Timecode(29.97, '00:59:59;29') 664 | assert tc.to_realtime() == str(Timecode(1000, '01:00:00.000') - int(round(3.6))) 665 | 666 | #- "[...] The deviation accumulated over a 24-hour period is approximately –2.6 frames (–86 ms)" 667 | tc = Timecode(59.94, '23:59:59;59') 668 | assert tc.to_realtime() == str(Timecode(1000, '24:00:00.000') - 86) 669 | 670 | 671 | def test_timestamp_realtime_4(): 672 | #SMPTE 12-1 §5.2.2 673 | #- "Monotonically counting at int_framerate will yield a deviation of approx. +3.6 s in one hour of elapsed time." 674 | tc = Timecode(59.94, '00:59:59:59', force_non_drop_frame=True) 675 | assert tc.to_realtime() == str(Timecode(1000, '01:00:00.000') + 3600) 676 | 677 | 678 | def test_timestamp_systemtime_1(): 679 | """ 680 | TC with integer framerate always have system time equal to elapsed time. 681 | """ 682 | tc50 = Timecode(50, '00:59:59:49') 683 | tc24 = Timecode(24, '00:59:59:23') 684 | tcms = Timecode(1000, '01:00:00.000') 685 | assert tc50.to_systemtime() == '01:00:00.000' 686 | assert tc24.to_systemtime() == '01:00:00.000' 687 | assert tcms.to_systemtime() == '01:00:00.000' 688 | 689 | 690 | def test_timestamp_systemtime_2(): 691 | """ 692 | TC with NTSC framerate always have system time different to realtime. 693 | """ 694 | tc = Timecode(23.98, '00:59:59:23') 695 | assert tc.to_systemtime() == '01:00:00.000' 696 | assert tc.to_systemtime() != tc.to_realtime() 697 | 698 | 699 | def test_timestamp_systemtime_3(): 700 | """ 701 | TC with DF NTSC framerate have system time roughly equal to real time. 702 | with a -3.6 ms drift per hour (SMPTE 12-1 §5.2.2). 703 | """ 704 | tc = Timecode(29.97, '23:59:59;29') 705 | assert tc.to_systemtime() == '24:00:00.000' 706 | #Check if we have the expected drift at 24h 707 | assert abs(tc.to_systemtime(True) - tc.to_realtime(True) - 24*3600e-6) < 1e-6 708 | 709 | 710 | def test_add_const_dropframe_flag(): 711 | tc1 = Timecode(29.97, "00:00:00:00", force_non_drop_frame=True) 712 | assert (tc1 + 1).drop_frame is False 713 | 714 | 715 | def test_add_tc_dropframe_flag(): 716 | tc1 = Timecode(29.97, "00:00:00:00", force_non_drop_frame=True) 717 | tc2 = Timecode(29.97, "00:00:00;00") 718 | 719 | # Left operand drop_frame flag is preserved 720 | assert (tc1 + tc2).drop_frame is False 721 | assert (tc2 + tc1).drop_frame is True 722 | 723 | 724 | def test_ge_overload(): 725 | tc1 = Timecode(24, "00:00:00:00") 726 | tc2 = Timecode(24, "00:00:00:00") 727 | tc3 = Timecode(24, "00:00:00:01") 728 | tc4 = Timecode(24, "00:00:01.100") 729 | tc5 = Timecode(24, "00:00:01.200") 730 | 731 | assert tc1 == tc2 732 | assert tc1 >= tc2 733 | assert tc3 >= tc2 734 | assert (tc2 >= tc3) is False 735 | assert tc4 <= tc5 736 | 737 | 738 | def test_gt_overload_a(): 739 | tc1 = Timecode(24, "00:00:00:00") 740 | tc2 = Timecode(24, "00:00:00:00") 741 | tc3 = Timecode(24, "00:00:00:01") 742 | tc4 = Timecode(24, "00:00:01.100") 743 | tc5 = Timecode(24, "00:00:01.200") 744 | 745 | assert not (tc1 > tc2) 746 | assert not (tc2 > tc2) 747 | assert tc3 > tc2 748 | assert tc5 > tc4 749 | 750 | 751 | def test_le_overload(): 752 | tc1 = Timecode(24, "00:00:00:00") 753 | tc2 = Timecode(24, "00:00:00:00") 754 | tc3 = Timecode(24, "00:00:00:01") 755 | tc4 = Timecode(24, "00:00:01.100") 756 | tc5 = Timecode(24, "00:00:01.200") 757 | 758 | assert (tc1 == tc2) 759 | assert (tc1 <= tc2) 760 | assert (tc2 <= tc3) 761 | assert not (tc2 >= tc3) 762 | assert (tc5 >= tc4) 763 | assert tc5 > tc4 764 | 765 | 766 | def test_gt_overload_b(): 767 | tc1 = Timecode(24, "00:00:00:00") 768 | tc2 = Timecode(24, "00:00:00:00") 769 | tc3 = Timecode(24, "00:00:00:01") 770 | tc4 = Timecode(24, "00:00:01.100") 771 | tc5 = Timecode(24, "00:00:01.200") 772 | 773 | assert not (tc1 < tc2) 774 | assert not (tc2 < tc2) 775 | assert (tc2 < tc3) 776 | assert (tc4 < tc5) 777 | 778 | 779 | def test_parse_timecode_with_int(): 780 | """parse_timecode method with int input.""" 781 | result = Timecode.parse_timecode(16663) 782 | assert result == (0, 0, 41, 17) # issue #16 783 | 784 | 785 | def test_frames_argument_is_not_an_int(): 786 | """TypeError is raised if the frames argument is not an integer.""" 787 | with pytest.raises(TypeError) as cm: 788 | Timecode("30", frames=0.1223) 789 | 790 | assert "Timecode.frames should be a positive integer bigger than zero, not a float" == str(cm.value) 791 | 792 | 793 | def test_frames_argument_is_zero(): 794 | """ValueError is raised if the frames argument is given as 0.""" 795 | with pytest.raises(ValueError) as cm: 796 | Timecode("30", frames=0) 797 | 798 | assert "Timecode.frames should be a positive integer bigger than zero, not 0" == str(cm.value) 799 | 800 | 801 | def test_bug_report_30(): 802 | """bug report 30 803 | 804 | The claim on the bug report was to get ``00:34:45:09`` from a Timecode with 23.976 805 | as the frame rate (supplied with Python 3's Fraction library) and 50000 as the total 806 | number of frames. The support for Fraction instances were missing, and it has been 807 | added. But the claim for the resultant Timecode was wrong, the resultant Timecode 808 | should have been ``00:34:43:07`` and that has been confirmed by DaVinci Resolve. 809 | """ 810 | from fractions import Fraction 811 | 812 | framerate = Fraction(24000, 1001) # 23.976023976023978 813 | frame_idx = 50000 814 | 815 | tc1 = Timecode(framerate, frames=frame_idx) 816 | assert "00:34:43:07" == tc1.__repr__() 817 | 818 | 819 | def test_bug_report_31_part1(): 820 | """bug report 31 821 | https://github.com/eoyilmaz/timecode/issues/31 822 | """ 823 | timecode1 = "01:00:10:00" 824 | timecode2 = "01:00:10:00" 825 | a = Timecode("25", timecode1) 826 | b = Timecode("25", timecode2) 827 | 828 | with pytest.raises(ValueError) as cm: 829 | _ = a - b 830 | 831 | assert ( 832 | str(cm.value) 833 | == "Timecode.frames should be a positive integer bigger than zero, not 0" 834 | ) 835 | 836 | 837 | def test_bug_report_31_part2(): 838 | """bug report 31 839 | https://github.com/eoyilmaz/timecode/issues/31 840 | """ 841 | timecode1 = "01:00:08:00" 842 | timecode2 = "01:00:10:00" 843 | timecode3 = "01:01:00:00" 844 | a = Timecode("25", timecode1) 845 | b = Timecode("25", timecode2) 846 | offset = a - b 847 | _ = Timecode("25", timecode3) + offset 848 | 849 | 850 | def test_bug_report_32(): 851 | """bug report 32 852 | https://github.com/eoyilmaz/timecode/issues/32 853 | """ 854 | framerate = "30000/1001" 855 | seconds = 500 856 | tc1 = Timecode(framerate, start_seconds=seconds) 857 | assert seconds == tc1.float 858 | 859 | 860 | def test_set_timecode_method(): 861 | """set_timecode method is working properly.""" 862 | tc1 = Timecode("24") 863 | assert tc1.frames == 1 864 | assert tc1 == "00:00:00:00" 865 | 866 | tc2 = Timecode("29.97", frames=1000) 867 | assert tc2.frames == 1000 868 | 869 | tc1.set_timecode(tc2.__repr__()) # this is interpreted as 24 870 | assert tc1.frames == 802 871 | 872 | tc1.set_timecode(tc2) # this should be interpreted as 29.97 and 1000 frames 873 | assert tc1.frames == 1000 874 | 875 | 876 | def test_iter_method(): 877 | """__iter__ method""" 878 | tc = Timecode("24", "01:00:00:00") 879 | for a in tc: 880 | assert a == tc 881 | 882 | 883 | def test_back_method_returns_a_timecode_instance(): 884 | """back method returns a Timecode instance.""" 885 | tc = Timecode("24", "01:00:00:00") 886 | assert isinstance(tc.back(), Timecode) 887 | 888 | 889 | def test_back_method_returns_the_instance_itself(): 890 | """back method returns the Timecode instance itself.""" 891 | tc = Timecode("24", "01:00:00:00") 892 | assert tc.back() is tc 893 | 894 | 895 | def test_back_method_reduces_frames_by_one(): 896 | """back method reduces the ``Timecode.frames`` by one.""" 897 | tc = Timecode("24", "01:00:00:00") 898 | frames = tc.frames 899 | assert tc.back().frames == (frames - 1) 900 | 901 | 902 | def test_mult_frames_method_is_working_properly(): 903 | """mult_frames method is working properly.""" 904 | tc = Timecode("24") 905 | tc.mult_frames(10) 906 | assert tc.frames == 10 907 | assert tc.__repr__() == "00:00:00:09" 908 | 909 | 910 | def test_div_frames_method_is_working_properly(): 911 | """div_frames method is working properly.""" 912 | tc = Timecode("24", "00:00:00:09") 913 | assert tc.frames == 10 914 | tc.div_frames(10) 915 | assert tc.frames == 1 916 | assert tc.__repr__() == "00:00:00:00" 917 | 918 | 919 | def test_eq_method_with_integers(): 920 | """Comparing the Timecode with integers are working properly.""" 921 | tc = Timecode("24", "00:00:10:00") 922 | assert tc == 241 923 | 924 | 925 | def test_ge_method_with_strings(): 926 | """__ge__ method with strings.""" 927 | tc = Timecode("24", "00:00:10:00") 928 | assert tc >= "00:00:09:00" 929 | assert tc >= "00:00:10:00" 930 | 931 | 932 | def test_ge_method_with_integers(): 933 | """__ge__ method with integers.""" 934 | tc = Timecode("24", "00:00:10:00") 935 | assert tc >= 230 936 | assert tc >= 241 937 | 938 | 939 | def test_gt_method_with_strings(): 940 | """__gt__ method with strings.""" 941 | tc = Timecode("24", "00:00:10:00") 942 | assert tc > "00:00:09:00" 943 | 944 | 945 | def test_gt_method_with_integers(): 946 | """__gt__ method with integers.""" 947 | tc = Timecode("24", "00:00:10:00") 948 | assert tc > 230 949 | 950 | 951 | def test_le_method_with_strings(): 952 | """__le__ method with strings.""" 953 | tc = Timecode("24", "00:00:10:00") 954 | assert tc <= "00:00:11:00" 955 | assert tc <= "00:00:10:00" 956 | 957 | 958 | def test_le_method_with_integers(): 959 | """__le__ method with integers.""" 960 | tc = Timecode("24", "00:00:10:00") 961 | assert tc <= 250 962 | assert tc <= 241 963 | 964 | 965 | def test_lt_method_with_strings(): 966 | """__lt__ method with strings.""" 967 | tc = Timecode("24", "00:00:10:00") 968 | assert tc < "00:00:11:00" 969 | 970 | 971 | def test_lt_method_with_integers(): 972 | """__lt__ method with integers.""" 973 | tc = Timecode("24", "00:00:10:00") 974 | assert tc < 250 975 | 976 | 977 | def test_fraction_lib_from_python3_raises_import_error_for_python2(): 978 | """ImportError is raised and the error is handled gracefully under Python 2 if 979 | importing the Fraction library which is introduced in Python 3. 980 | 981 | This is purely done for increasing the code coverage to 100% under Python 3. 982 | """ 983 | try: 984 | import mock 985 | except ImportError: 986 | from unittest import mock 987 | import sys 988 | 989 | with mock.patch.dict(sys.modules, {"fractions": None}): 990 | # the coverage should be now 100% 991 | _ = Timecode("24") 992 | 993 | 994 | def test_rollover_for_23_98(): 995 | """bug report #33.""" 996 | tc = Timecode("23.98", "23:58:47:00") 997 | assert 2071849 == tc.frames 998 | tc.add_frames(24) 999 | assert 2071873 == tc.frames 1000 | assert "23:58:48:00" == tc.__repr__() 1001 | 1002 | 1003 | @pytest.mark.parametrize( 1004 | "args,kwargs,str_repr", [ 1005 | [["29.97"], {"frames": 2589408}, "23:59:59;29"], 1006 | [["29.97"], {"frames": 2589409}, "00:00:00;00"], 1007 | [["29.97"], {"frames": 2589409, "force_non_drop_frame": True}, "23:58:33:18"], 1008 | [["29.97"], {"frames": 2592001, "force_non_drop_frame": True}, "00:00:00:00"], 1009 | [["59.94"], {"frames": 5178816}, "23:59:59;59"], 1010 | [["59.94"], {"frames": 5178817}, "00:00:00;00"], 1011 | [["59.94"], {"frames": 5184000, "force_non_drop_frame": True}, "23:59:59:59"], 1012 | [["59.94"], {"frames": 5184001, "force_non_drop_frame": True}, "00:00:00:00"], 1013 | ] 1014 | ) 1015 | def test_rollover(args, kwargs, str_repr): 1016 | tc = Timecode(*args, **kwargs) 1017 | assert str_repr == tc.__str__() 1018 | -------------------------------------------------------------------------------- /timecode/__init__.py: -------------------------------------------------------------------------------- 1 | #!-*- coding: utf-8 -*- 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014 Joshua Banton and PyTimeCode developers 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | __name__ = "timecode" 25 | __version__ = "1.4.1" 26 | __description__ = "SMPTE Time Code Manipulation Library" 27 | __author__ = "Erkan Ozgur Yilmaz" 28 | __author_email__ = "eoyilmaz@gmail.com" 29 | __url__ = "https://github.com/eoyilmaz/timecode" 30 | 31 | 32 | from fractions import Fraction 33 | import sys 34 | from typing import Optional, Tuple, Union, overload 35 | 36 | try: 37 | from typing import Literal 38 | except ImportError: 39 | pass 40 | 41 | 42 | class Timecode(object): 43 | """The main timecode class. 44 | 45 | Does all the calculation over frames, so the main data it holds is frames, then when 46 | required it converts the frames to a timecode by using the frame rate setting. 47 | 48 | Args: 49 | framerate (Union[str, int, float, Fraction]): The frame rate of the Timecode 50 | instance. If a str is given it should be one of ['23.976', '23.98', '24', 51 | '25', '29.97', '30', '50', '59.94', '60', 'NUMERATOR/DENOMINATOR', ms'] 52 | where "ms" equals to 1000 fps. Otherwise, any integer or Fractional value is 53 | accepted. Can not be skipped. Setting the framerate will automatically set 54 | the :attr:`.drop_frame` attribute to correct value. 55 | start_timecode (Union[None, str]): The start timecode. Use this to be able to 56 | set the timecode of this Timecode instance. It can be skipped and then the 57 | frames attribute will define the timecode, and if it is also skipped then 58 | the start_second attribute will define the start timecode, and if 59 | start_seconds is also skipped then the default value of '00:00:00:00' will 60 | be used. When using 'ms' frame rate, timecodes like '00:11:01.040' use 61 | '.040' as frame number. When used with other frame rates, '.040' represents 62 | a fraction of a second. So '00:00:00.040' at 25fps is 1 frame. 63 | start_seconds (Union[int, float]): A float or integer value showing the seconds. 64 | frames (int): Timecode objects can be initialized with an integer number showing 65 | the total frames. 66 | force_non_drop_frame (bool): If True, uses Non-Dropframe calculation for 29.97 67 | or 59.94 only. Has no meaning for any other framerate. It is False by 68 | default. 69 | """ 70 | 71 | def __init__( 72 | self, 73 | framerate: Union[str, int, float, Fraction], 74 | start_timecode: Optional[str] = None, 75 | start_seconds: Optional[Union[int, float]] = None, 76 | frames: Optional[int] = None, 77 | force_non_drop_frame: bool = False, 78 | ): 79 | 80 | self.force_non_drop_frame = force_non_drop_frame 81 | 82 | self.drop_frame = False 83 | 84 | self.ms_frame = False 85 | self.fraction_frame = False 86 | self._int_framerate : Union[None, int] = None 87 | self._framerate : Union[None, str, int, float, Fraction] = None 88 | self.framerate = framerate # type: ignore 89 | self._frames : Union[None, int] = None 90 | 91 | # attribute override order 92 | # start_timecode > frames > start_seconds 93 | if start_timecode: 94 | self.frames = self.tc_to_frames(start_timecode) 95 | else: 96 | if frames is not None: 97 | self.frames = frames 98 | elif start_seconds is not None: 99 | if start_seconds == 0: 100 | raise ValueError("``start_seconds`` argument can not be 0") 101 | self.frames = self.float_to_tc(start_seconds) 102 | else: 103 | # use default value of 00:00:00:00 104 | self.frames = self.tc_to_frames("00:00:00:00") 105 | 106 | @property 107 | def frames(self) -> int: 108 | """Return the _frames attribute value. 109 | 110 | Returns: 111 | int: The frames attribute value. 112 | """ 113 | return self._frames # type: ignore 114 | 115 | @frames.setter 116 | def frames(self, frames: int) -> None: 117 | """Set the_frames attribute. 118 | 119 | Args: 120 | frames (int): A positive int bigger than zero showing the number of frames 121 | that this Timecode represents. 122 | """ 123 | # validate the frames value 124 | if not isinstance(frames, int): 125 | raise TypeError( 126 | "%s.frames should be a positive integer bigger " 127 | "than zero, not a %s" 128 | % (self.__class__.__name__, frames.__class__.__name__) 129 | ) 130 | 131 | if frames <= 0: 132 | raise ValueError( 133 | "%s.frames should be a positive integer bigger " 134 | "than zero, not %s" % (self.__class__.__name__, frames) 135 | ) 136 | self._frames = frames 137 | 138 | @property 139 | def framerate(self) -> str: 140 | """Return the _framerate attribute. 141 | 142 | Returns: 143 | str: The frame rate of this Timecode instance. 144 | """ 145 | return self._framerate # type: ignore 146 | 147 | @framerate.setter 148 | def framerate(self, framerate: Union[int, float, str, Tuple[int, int], Fraction]) -> None: 149 | """Set the framerate attribute. 150 | 151 | Args: 152 | framerate (Union[int, float, str, tuple, Fraction]): Several different type 153 | is accepted for this argument: 154 | 155 | int, float: It is directly used. 156 | str: Is used for setting DF Timecodes and possible values are 157 | ["23.976", "23.98", "29.97", "59.94", "ms", "1000", "frames"] where 158 | "ms" and "1000" results in to a milliseconds based Timecode and 159 | "frames" will result a Timecode with 1 FPS. 160 | tuple: The tuple should be in (nominator, denominator) format in which 161 | the frame rate is kept as a fraction. 162 | Fraction: If the current version of Python supports (which it should) 163 | then Fraction is also accepted. 164 | """ 165 | # Convert rational frame rate to float, defaults to None if not Fraction-like 166 | numerator = getattr(framerate, 'numerator', None) 167 | denominator = getattr(framerate, 'denominator', None) 168 | 169 | try: 170 | if "/" in framerate: # type: ignore 171 | numerator, denominator = framerate.split("/") # type: ignore 172 | except TypeError: 173 | # not a string 174 | pass 175 | 176 | if isinstance(framerate, tuple): 177 | numerator, denominator = framerate 178 | 179 | if numerator and denominator: 180 | framerate = round(float(numerator) / float(denominator), 2) 181 | if framerate.is_integer(): 182 | framerate = int(framerate) 183 | 184 | # check if number is passed and if so convert it to a string 185 | if isinstance(framerate, (int, float)): 186 | framerate = str(framerate) 187 | 188 | self._ntsc_framerate = False 189 | 190 | # set the int_frame_rate 191 | if framerate == "29.97": 192 | self._int_framerate = 30 193 | self.drop_frame = not self.force_non_drop_frame 194 | self._ntsc_framerate = True 195 | elif framerate == "59.94": 196 | self._int_framerate = 60 197 | self.drop_frame = not self.force_non_drop_frame 198 | self._ntsc_framerate = True 199 | elif any(map(lambda x: framerate.startswith(x), ["23.976", "23.98"])): # type: ignore 200 | self._int_framerate = 24 201 | self._ntsc_framerate = True 202 | elif framerate in ["ms", "1000"]: 203 | self._int_framerate = 1000 204 | self.ms_frame = True 205 | framerate = 1000 206 | elif framerate == "frames": 207 | self._int_framerate = 1 208 | else: 209 | self._int_framerate = int(float(framerate)) # type: ignore 210 | 211 | self._framerate = framerate # type: ignore 212 | 213 | def set_fractional(self, state: bool) -> None: 214 | """Set if the Timecode is to be represented with fractional seconds. 215 | 216 | Args: 217 | state (bool): If set to True the current Timecode instance will be 218 | represented with a fractional seconds (will have a "." in the frame 219 | separator). 220 | """ 221 | self.fraction_frame = state 222 | 223 | def set_timecode(self, timecode: Union[str, "Timecode"]) -> None: 224 | """Set the frames by using the given timecode. 225 | 226 | Args: 227 | timecode (Union[str, Timecode]): Either a str representation of a Timecode 228 | or a Timecode instance. 229 | """ 230 | self.frames = self.tc_to_frames(timecode) 231 | 232 | def float_to_tc(self, seconds: float) -> int: 233 | """Return the number of frames in the given seconds using the current instance. 234 | 235 | Args: 236 | seconds (float): The seconds to set hte timecode to. This uses the integer 237 | frame rate for proper calculation. 238 | 239 | Returns: 240 | int: The number of frames in the given seconds.ß 241 | """ 242 | return int(seconds * self._int_framerate) 243 | 244 | def tc_to_frames(self, timecode: Union[str, "Timecode"]) -> int: 245 | """Convert the given Timecode to frames. 246 | 247 | Args: 248 | timecode (Union[str, Timecode]): Either a str representing a Timecode or a 249 | Timecode instance. 250 | 251 | Returns: 252 | int: The number of frames in the given Timecode. 253 | """ 254 | # timecode could be a Timecode instance 255 | if isinstance(timecode, Timecode): 256 | return timecode.frames 257 | 258 | hours, minutes, seconds, frames = map(int, self.parse_timecode(timecode)) 259 | 260 | if isinstance(timecode, int): 261 | time_tokens = [hours, minutes, seconds, frames] 262 | timecode = ":".join(str(t) for t in time_tokens) 263 | 264 | if self.drop_frame: 265 | timecode = ";".join(timecode.rsplit(":", 1)) 266 | 267 | if self.framerate != "frames": 268 | ffps = float(self.framerate) 269 | else: 270 | ffps = float(self._int_framerate) 271 | 272 | if self.drop_frame: 273 | # Number of drop frames is 6% of framerate rounded to nearest 274 | # integer 275 | drop_frames = int(round(ffps * 0.066666)) 276 | else: 277 | drop_frames = 0 278 | 279 | # We don't need the exact framerate anymore, we just need it rounded to 280 | # nearest integer 281 | ifps = self._int_framerate 282 | 283 | # Number of frames per hour (non-drop) 284 | hour_frames = ifps * 60 * 60 285 | 286 | # Number of frames per minute (non-drop) 287 | minute_frames = ifps * 60 288 | 289 | # Total number of minutes 290 | total_minutes = (60 * hours) + minutes 291 | 292 | # Handle case where frames are fractions of a second 293 | if len(timecode.split(".")) == 2 and not self.ms_frame: 294 | self.fraction_frame = True 295 | fraction = timecode.rsplit(".", 1)[1] 296 | 297 | frames = int(round(float("." + fraction) * ffps)) 298 | 299 | frame_number = ( 300 | (hour_frames * hours) 301 | + (minute_frames * minutes) 302 | + (ifps * seconds) 303 | + frames 304 | ) - (drop_frames * (total_minutes - (total_minutes // 10))) 305 | 306 | return frame_number + 1 # frames 307 | 308 | def frames_to_tc(self, frames: int, skip_rollover: bool = False) -> Tuple[int, int, int, Union[float, int]]: 309 | """Convert frames back to timecode. 310 | 311 | Args: 312 | frames (int): Number of frames. 313 | 314 | Returns: 315 | tuple: A tuple containing the hours, minutes, seconds and frames 316 | """ 317 | if self.drop_frame: 318 | # Number of frames to drop on the minute marks is the nearest 319 | # integer to 6% of the framerate 320 | ffps = float(self.framerate) 321 | drop_frames = int(round(ffps * 0.066666)) 322 | else: 323 | ffps = float(self._int_framerate) 324 | drop_frames = 0 325 | 326 | # Number of frames per ten minutes 327 | frames_per_10_minutes = int(round(ffps * 60 * 10)) 328 | 329 | # Number of frames in a day - timecode rolls over after 24 hours 330 | frames_per_24_hours = int(round(ffps * 60 * 60 * 24)) 331 | 332 | # Number of frames per minute is the round of the framerate * 60 minus 333 | # the number of dropped frames 334 | frames_per_minute = int(round(ffps) * 60) - drop_frames 335 | 336 | frame_number = frames - 1 337 | 338 | # If frame_number is greater than 24 hrs, next operation will rollover 339 | # clock 340 | if not skip_rollover: 341 | frame_number %= frames_per_24_hours 342 | 343 | if self.drop_frame: 344 | d = frame_number // frames_per_10_minutes 345 | m = frame_number % frames_per_10_minutes 346 | if m > drop_frames: 347 | frame_number += (drop_frames * 9 * d) + drop_frames * ( 348 | (m - drop_frames) // frames_per_minute 349 | ) 350 | else: 351 | frame_number += drop_frames * 9 * d 352 | 353 | ifps = self._int_framerate 354 | 355 | frs: Union[int, float] = frame_number % ifps 356 | if self.fraction_frame: 357 | frs = round(frs / float(ifps), 3) 358 | 359 | secs = int((frame_number // ifps) % 60) 360 | mins = int(((frame_number // ifps) // 60) % 60) 361 | hrs = int((((frame_number // ifps) // 60) // 60)) 362 | 363 | return hrs, mins, secs, frs 364 | 365 | def tc_to_string(self, hrs: int, mins: int, secs: int, frs: Union[float, int]) -> str: 366 | """Return the string representation of a Timecode with given info. 367 | 368 | Args: 369 | hrs (int): The hours portion of the Timecode. 370 | mins (int): The minutes portion of the Timecode. 371 | secs (int): The seconds portion of the Timecode. 372 | frs (int | float): The frames portion of the Timecode. 373 | 374 | Returns: 375 | str: The string representation of this Timecode.ßß 376 | """ 377 | if self.fraction_frame: 378 | return "{hh:02d}:{mm:02d}:{ss:06.3f}".format(hh=hrs, mm=mins, ss=secs + frs) 379 | 380 | ff = "{:02d}" 381 | if self.ms_frame: 382 | ff = "{:03d}" 383 | 384 | return ("{:02d}:{:02d}:{:02d}{}" + ff).format( 385 | hrs, mins, secs, self.frame_delimiter, frs 386 | ) 387 | 388 | # to maintain python 3.7 compatibility (no literal type yet!) 389 | # only use overload in 3.8+ 390 | if sys.version_info >= (3, 8): 391 | @overload 392 | def to_systemtime(self, as_float: Literal[True]) -> float: 393 | pass 394 | 395 | @overload 396 | def to_systemtime(self, as_float: Literal[False]) -> str: 397 | pass 398 | 399 | def to_systemtime(self, as_float: bool = False) -> Union[str, float]: # type:ignore 400 | """Convert a Timecode to the video system timestamp. 401 | For NTSC rates, the video system time is not the wall-clock one. 402 | 403 | Args: 404 | as_float (bool): Return the time as a float number of seconds. 405 | 406 | Returns: 407 | str: The "system time" timestamp of the Timecode. 408 | """ 409 | if self.ms_frame: 410 | return self.float-(1e-3) if as_float else str(self) 411 | 412 | hh, mm, ss, ff = self.frames_to_tc(self.frames + 1, skip_rollover=True) 413 | framerate = float(self.framerate) if self._ntsc_framerate else self._int_framerate 414 | ms = ff/framerate 415 | if as_float: 416 | return (hh*3600 + mm*60 + ss + ms) 417 | return "{:02d}:{:02d}:{:02d}.{:03d}".format(hh, mm, ss, round(ms*1000)) 418 | 419 | # to maintain python 3.7 compatibility (no literal type yet!) 420 | # only use typing.Literal in 3.8+ 421 | if sys.version_info >= (3, 8): 422 | @overload # type: ignore # noqa 423 | def to_realtime(self, as_float: Literal[True]) -> float: 424 | pass 425 | 426 | @overload # type: ignore # noqa 427 | def to_realtime(self, as_float: Literal[False]) -> str: 428 | pass 429 | 430 | def to_realtime(self, as_float: bool = False) -> Union[str, float]: # type:ignore 431 | """Convert a Timecode to a "real time" timestamp. 432 | Reference: SMPTE 12-1 §5.1.2 433 | 434 | Args: 435 | as_float (bool): Return the time as a float number of seconds. 436 | 437 | Returns: 438 | str: The "real time" timestamp of the Timecode. 439 | """ 440 | #float property is in the video system time grid 441 | ts_float = self.float 442 | 443 | if self.ms_frame: 444 | return ts_float-(1e-3) if as_float else str(self) 445 | 446 | # "int_framerate" frames is one second in NTSC time 447 | if self._ntsc_framerate: 448 | ts_float *= 1.001 449 | if as_float: 450 | return ts_float 451 | 452 | f_fmtdivmod = lambda x: (int(x[0]), x[1]) 453 | hh, ts_float = f_fmtdivmod(divmod(ts_float, 3600)) 454 | mm, ts_float = f_fmtdivmod(divmod(ts_float, 60)) 455 | ss, ts_float = f_fmtdivmod(divmod(ts_float, 1)) 456 | ms = round(ts_float*1000) 457 | 458 | return "{:02d}:{:02d}:{:02d}.{:03d}".format(hh, mm, ss, ms) 459 | 460 | @classmethod 461 | def parse_timecode(cls, timecode: Union[int, str]) -> Tuple[int, int, int, int]: 462 | """Parse the given timecode string. 463 | 464 | This uses the frame separator do decide if this is a NDF, DF or a 465 | or milliseconds/fraction_of_seconds based Timecode. 466 | 467 | '00:00:00:00' will result a NDF Timecode where, '00:00:00;00' will result a DF 468 | Timecode or '00:00:00.000' will be a milliseconds/fraction_of_seconds based 469 | Timecode. 470 | 471 | Args: 472 | timecode (Union[int, str]): If an integer is given it is converted to hex 473 | and the hours, minutes, seconds and frames are extracted from the hex 474 | representation. If a str is given it should follow one of the SMPTE 475 | timecode formats.ß 476 | 477 | Returns: 478 | (int, int, int, int): A tuple containing the hours, minutes, seconds and 479 | frames part of the Timecode. 480 | """ 481 | if isinstance(timecode, int): 482 | hex_repr = hex(timecode) 483 | # fix short string 484 | hex_repr = "0x%s" % (hex_repr[2:].zfill(8)) 485 | hrs, mins, secs, frs = tuple( 486 | map(int, [hex_repr[i : i + 2] for i in range(2, 10, 2)]) 487 | ) 488 | 489 | else: 490 | bfr = timecode.replace(";", ":").replace(".", ":").split(":") 491 | hrs = int(bfr[0]) 492 | mins = int(bfr[1]) 493 | secs = int(bfr[2]) 494 | frs = int(bfr[3]) 495 | 496 | return hrs, mins, secs, frs 497 | 498 | @property 499 | def frame_delimiter(self) -> str: 500 | """Return correct frame deliminator symbol based on the framerate. 501 | 502 | Returns: 503 | str: The frame deliminator, ";" if this is a drop frame timecode, "." if 504 | this is a millisecond based Timecode or ":" in any other case. 505 | """ 506 | if self.drop_frame: 507 | return ";" 508 | 509 | elif self.ms_frame or self.fraction_frame: 510 | return "." 511 | 512 | else: 513 | return ":" 514 | 515 | def __iter__(self): 516 | """Yield and iterator. 517 | 518 | Yields: 519 | Timecode: Yields this Timecode instance. 520 | """ 521 | yield self 522 | 523 | def next(self) -> "Timecode": 524 | """Add one frame to this Timecode to go the next frame. 525 | 526 | Returns: 527 | Timecode: Returns self. So, this is the same Timecode instance with this 528 | one. 529 | """ 530 | self.add_frames(1) 531 | return self 532 | 533 | def back(self) -> "Timecode": 534 | """Subtract one frame from this Timecode to go back one frame. 535 | 536 | Returns: 537 | Timecode: Returns self. So, this is the same Timecode instance with this 538 | one. 539 | """ 540 | self.sub_frames(1) 541 | return self 542 | 543 | def add_frames(self, frames: int) -> None: 544 | """Add or subtract frames from the number of frames of this Timecode. 545 | 546 | Args: 547 | frames (int): The number to subtract from or add to the number of frames of 548 | this Timecode instance. 549 | """ 550 | self.frames += frames 551 | 552 | def sub_frames(self, frames: int) -> None: 553 | """Add or subtract frames from the number of frames of this Timecode. 554 | 555 | Args: 556 | frames (int): The number to subtract from or add to the number of frames of 557 | this Timecode instance. 558 | """ 559 | self.add_frames(-frames) 560 | 561 | def mult_frames(self, frames: int) -> None: 562 | """Multiply frames. 563 | 564 | Args: 565 | frames (int): Multiply the frames with this number. 566 | """ 567 | self.frames *= frames 568 | 569 | def div_frames(self, frames: int) -> None: 570 | """Divide the number of frames to the given number. 571 | 572 | Args: 573 | frames (int): The other number to divide the number of frames of this 574 | Timecode instance to. 575 | """ 576 | self.frames = int(self.frames / frames) 577 | 578 | def __eq__(self, other: Union[int, str, "Timecode", object]) -> bool: 579 | """Override the equality operator. 580 | 581 | Args: 582 | other (Union[int, str, Timecode]): Either and int representing the number of 583 | frames, a str representing the start time of a Timecode with the same 584 | frame rate of this one, or a Timecode to compare with the number of 585 | frames. 586 | 587 | Returns: 588 | bool: True if the other is equal to this Timecode instance. 589 | """ 590 | if isinstance(other, Timecode): 591 | return self.framerate == other.framerate and self.frames == other.frames 592 | elif isinstance(other, str): 593 | new_tc = Timecode(self.framerate, other) 594 | return self.__eq__(new_tc) 595 | elif isinstance(other, int): 596 | return self.frames == other 597 | else: 598 | return False 599 | 600 | def __ge__(self, other: Union[int, str, "Timecode", object]) -> bool: 601 | """Override greater than or equal to operator. 602 | 603 | Args: 604 | other (Union[int, str, Timecode]): Either and int representing the number of 605 | frames, a str representing the start time of a Timecode with the same 606 | frame rate of this one, or a Timecode to compare with the number of 607 | frames. 608 | 609 | Returns: 610 | bool: True if the other is greater than or equal to this Timecode instance. 611 | """ 612 | if isinstance(other, Timecode): 613 | return self.framerate == other.framerate and self.frames >= other.frames 614 | elif isinstance(other, str): 615 | new_tc = Timecode(self.framerate, other) 616 | return self.frames >= new_tc.frames 617 | elif isinstance(other, int): 618 | return self.frames >= other 619 | else: 620 | raise TypeError( 621 | "'>=' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) 622 | ) 623 | 624 | def __gt__(self, other: Union[int, str, "Timecode"]) -> bool: 625 | """Override greater than operator. 626 | 627 | Args: 628 | other (Union[int, str, Timecode]): Either and int representing the number of 629 | frames, a str representing the start time of a Timecode with the same 630 | frame rate of this one, or a Timecode to compare with the number of 631 | frames. 632 | 633 | Returns: 634 | bool: True if the other is greater than this Timecode instance. 635 | """ 636 | if isinstance(other, Timecode): 637 | return self.framerate == other.framerate and self.frames > other.frames 638 | elif isinstance(other, str): 639 | new_tc = Timecode(self.framerate, other) 640 | return self.frames > new_tc.frames 641 | elif isinstance(other, int): 642 | return self.frames > other 643 | else: 644 | raise TypeError( 645 | "'>' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) 646 | ) 647 | 648 | def __le__(self, other: Union[int, str, "Timecode", object]) -> bool: 649 | """Override less or equal to operator. 650 | 651 | Args: 652 | other (Union[int, str, Timecode]): Either and int representing the number of 653 | frames, a str representing the start time of a Timecode with the same 654 | frame rate of this one, or a Timecode to compare with the number of 655 | frames. 656 | 657 | Returns: 658 | bool: True if the other is less than or equal to this Timecode instance. 659 | """ 660 | if isinstance(other, Timecode): 661 | return self.framerate == other.framerate and self.frames <= other.frames 662 | elif isinstance(other, str): 663 | new_tc = Timecode(self.framerate, other) 664 | return self.frames <= new_tc.frames 665 | elif isinstance(other, int): 666 | return self.frames <= other 667 | else: 668 | raise TypeError( 669 | "'<' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) 670 | ) 671 | 672 | def __lt__(self, other: Union[int, str, "Timecode"]) -> bool: 673 | """Override less than operator. 674 | 675 | Args: 676 | other (Union[int, str, Timecode]): Either and int representing the number of 677 | frames, a str representing the start time of a Timecode with the same 678 | frame rate of this one, or a Timecode to compare with the number of 679 | frames. 680 | 681 | Returns: 682 | bool: True if the other is less than this Timecode instance. 683 | """ 684 | if isinstance(other, Timecode): 685 | return self.framerate == other.framerate and self.frames < other.frames 686 | elif isinstance(other, str): 687 | new_tc = Timecode(self.framerate, other) 688 | return self.frames < new_tc.frames 689 | elif isinstance(other, int): 690 | return self.frames < other 691 | else: 692 | raise TypeError( 693 | "'<=' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) 694 | ) 695 | 696 | def __add__(self, other: Union["Timecode", int]) -> "Timecode": 697 | """Return a new Timecode with the given timecode or frames added to this one. 698 | 699 | Args: 700 | other (Union[int, Timecode]): Either and int value or a Timecode in which 701 | the frames are used for the calculation. 702 | 703 | Raises: 704 | TimecodeError: If the other is not an int or Timecode. 705 | 706 | Returns: 707 | Timecode: The resultant Timecode instance. 708 | """ 709 | # duplicate current one 710 | tc = Timecode(self.framerate, frames=self.frames) 711 | tc.drop_frame = self.drop_frame 712 | 713 | if isinstance(other, Timecode): 714 | tc.add_frames(other.frames) 715 | elif isinstance(other, int): 716 | tc.add_frames(other) 717 | else: 718 | raise TimecodeError( 719 | "Type {} not supported for arithmetic.".format(other.__class__.__name__) 720 | ) 721 | 722 | return tc 723 | 724 | def __sub__(self, other: Union["Timecode", int]) -> "Timecode": 725 | """Return a new Timecode instance with subtracted value. 726 | 727 | Args: 728 | other (Union[int, Timecode]): The number to subtract, either an integer or 729 | another Timecode in which the number of frames is subtracted. 730 | 731 | Raises: 732 | TimecodeError: If the other is not an int or Timecode. 733 | 734 | Returns: 735 | Timecode: The resultant Timecode instance. 736 | """ 737 | if isinstance(other, Timecode): 738 | subtracted_frames = self.frames - other.frames 739 | elif isinstance(other, int): 740 | subtracted_frames = self.frames - other 741 | else: 742 | raise TimecodeError( 743 | "Type {} not supported for arithmetic.".format(other.__class__.__name__) 744 | ) 745 | tc = Timecode(self.framerate, frames=abs(subtracted_frames)) 746 | tc.drop_frame = self.drop_frame 747 | return tc 748 | 749 | def __mul__(self, other: Union["Timecode", int]) -> "Timecode": 750 | """Return a new Timecode instance with multiplied value. 751 | 752 | Args: 753 | other (Union[int, Timecode]): The multiplier either an integer or another 754 | Timecode in which the number of frames is used as the multiplier. 755 | 756 | Raises: 757 | TimecodeError: If the other is not an int or Timecode. 758 | 759 | Returns: 760 | Timecode: The resultant Timecode instance. 761 | """ 762 | if isinstance(other, Timecode): 763 | multiplied_frames = self.frames * other.frames 764 | elif isinstance(other, int): 765 | multiplied_frames = self.frames * other 766 | else: 767 | raise TimecodeError( 768 | "Type {} not supported for arithmetic.".format(other.__class__.__name__) 769 | ) 770 | tc = Timecode(self.framerate, frames=multiplied_frames) 771 | tc.drop_frame = self.drop_frame 772 | return tc 773 | 774 | def __div__(self, other: Union["Timecode", int]) -> "Timecode": 775 | """Return a new Timecode instance with divided value. 776 | 777 | Args: 778 | other (Union[int, Timecode]): The denominator either an integer or another 779 | Timecode in which the number of frames is used as the denominator. 780 | 781 | Raises: 782 | TimecodeError: If the other is not an int or Timecode. 783 | 784 | Returns: 785 | Timecode: The resultant Timecode instance. 786 | """ 787 | if isinstance(other, Timecode): 788 | div_frames = int(float(self.frames) / float(other.frames)) 789 | elif isinstance(other, int): 790 | div_frames = int(float(self.frames) / float(other)) 791 | else: 792 | raise TimecodeError( 793 | "Type {} not supported for arithmetic.".format(other.__class__.__name__) 794 | ) 795 | 796 | return Timecode(self.framerate, frames=div_frames) 797 | 798 | def __truediv__(self, other: Union["Timecode", int]) -> "Timecode": 799 | """Return a new Timecode instance with divided value. 800 | 801 | Args: 802 | other (Union[int, Timecode]): The denominator either an integer or another 803 | Timecode in which the number of frames is used as the denominator. 804 | 805 | Returns: 806 | Timecode: The resultant Timecode instance. 807 | """ 808 | return self.__div__(other) 809 | 810 | def __repr__(self): 811 | """Return the string representation of this Timecode instance. 812 | 813 | Returns: 814 | str: The string representation of this Timecode instance. 815 | """ 816 | return self.tc_to_string(*self.frames_to_tc(self.frames)) 817 | 818 | @property 819 | def hrs(self) -> int: 820 | """Return the hours part of the timecode. 821 | 822 | Returns: 823 | int: The hours part of the timecode. 824 | """ 825 | hrs, mins, secs, frs = self.frames_to_tc(self.frames) 826 | return hrs 827 | 828 | @property 829 | def mins(self) -> int: 830 | """Return the minutes part of the timecode. 831 | 832 | Returns: 833 | int: The minutes part of the timecode. 834 | """ 835 | hrs, mins, secs, frs = self.frames_to_tc(self.frames) 836 | return mins 837 | 838 | @property 839 | def secs(self) -> int: 840 | """Return the seconds part of the timecode. 841 | 842 | Returns: 843 | int: The seconds part of the timecode. 844 | """ 845 | hrs, mins, secs, frs = self.frames_to_tc(self.frames) 846 | return secs 847 | 848 | @property 849 | def frs(self) -> Union[float, int]: 850 | """Return the frames part of the timecode. 851 | 852 | Returns: 853 | int: The frames part of the timecode. 854 | """ 855 | hrs, mins, secs, frs = self.frames_to_tc(self.frames) 856 | return frs 857 | 858 | @property 859 | def frame_number(self) -> int: 860 | """Return the 0-based frame number of the current timecode instance. 861 | 862 | Returns: 863 | int: 0-based frame number. 864 | """ 865 | return self.frames - 1 866 | 867 | @property 868 | def float(self) -> float: 869 | """Return the seconds as float. 870 | 871 | Returns: 872 | float: The seconds as float. 873 | """ 874 | return float(self.frames) / float(self._int_framerate) 875 | 876 | 877 | class TimecodeError(Exception): 878 | """Raised when an error occurred in timecode calculation.""" 879 | -------------------------------------------------------------------------------- /timecode/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/timecode/224088b48352479201d4c4c6cc4711556dea3bf4/timecode/py.typed -------------------------------------------------------------------------------- /upload_to_pypi: -------------------------------------------------------------------------------- 1 | python -m build 2 | twine check dist/timecode-*.tar.gz 3 | twine upload dist/timecode-*.tar.gz 4 | -------------------------------------------------------------------------------- /upload_to_pypi.cmd: -------------------------------------------------------------------------------- 1 | del *.pyc /S 2 | ..\Scripts\python setup.py clean --all 3 | # ..\Scripts\python setup.py sdist upload 4 | ..\Scripts\python setup.py sdist bdist_wheel upload 5 | --------------------------------------------------------------------------------