mirror of
https://github.com/morpheus65535/bazarr
synced 2024-12-21 23:32:31 +00:00
Updated pysubs2 to 1.7.2
* chore: Bump pysubs2 to v1.7.1 * chore: bump version to 1.7.2 * remove bin
This commit is contained in:
parent
a8c1794061
commit
2b19f390e7
31 changed files with 637 additions and 413 deletions
|
@ -13,7 +13,7 @@ import pysrt
|
|||
import pysubs2
|
||||
from bs4 import UnicodeDammit
|
||||
from pysubs2 import SSAStyle
|
||||
from pysubs2.subrip import parse_tags, MAX_REPRESENTABLE_TIME
|
||||
from pysubs2.formats.subrip import parse_tags, MAX_REPRESENTABLE_TIME
|
||||
from pysubs2.time import ms_to_times
|
||||
from subzero.modification import SubtitleModifications
|
||||
from subzero.language import Language
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
Metadata-Version: 2.1
|
||||
Name: pysubs2
|
||||
Version: 1.6.1
|
||||
Summary: A library for editing subtitle files
|
||||
Home-page: https://github.com/tkarabela/pysubs2
|
||||
Author: Tomas Karabela
|
||||
Author-email: tkarabela@seznam.cz
|
||||
License: MIT
|
||||
Project-URL: Documentation, https://pysubs2.readthedocs.io
|
||||
Project-URL: Bug Tracker, https://github.com/tkarabela/pysubs2/issues
|
||||
Keywords: SubStation,SubRip,MicroDVD,MPL2,WebVTT,ass,srt,sub,vtt,subtitles,captions
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Classifier: Topic :: Text Processing :: Markup
|
||||
Classifier: Topic :: Multimedia :: Video
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Intended Audience :: End Users/Desktop
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE.txt
|
||||
|
||||
pysubs2
|
||||
=======
|
||||
|
||||
|
||||
.. image:: https://img.shields.io/github/actions/workflow/status/tkarabela/pysubs2/main.yml?branch=master
|
||||
:alt: pysubs2 build master branch
|
||||
:target: https://github.com/tkarabela/pysubs2/actions
|
||||
.. image:: https://img.shields.io/codecov/c/github/tkarabela/pysubs2
|
||||
:alt: pysubs2 test code coverage
|
||||
:target: https://app.codecov.io/github/tkarabela/pysubs2
|
||||
.. image:: http://www.mypy-lang.org/static/mypy_badge.svg
|
||||
:alt: MyPy checked
|
||||
:target: https://github.com/tkarabela/pysubs2/actions
|
||||
.. image:: https://img.shields.io/pypi/v/pysubs2.svg?style=flat-square
|
||||
:alt: PyPI - Version
|
||||
:target: https://pypi.org/project/pysubs2/
|
||||
.. image:: https://img.shields.io/pypi/status/pysubs2.svg?style=flat-square
|
||||
:alt: PyPI - Status
|
||||
:target: https://pypi.org/project/pysubs2/
|
||||
.. image:: https://img.shields.io/pypi/pyversions/pysubs2.svg?style=flat-square
|
||||
:alt: PyPI - Python Version
|
||||
:target: https://pypi.org/project/pysubs2/
|
||||
.. image:: https://img.shields.io/pypi/l/pysubs2.svg?style=flat-square
|
||||
:alt: PyPI - License
|
||||
:target: LICENSE.txt
|
||||
|
||||
|
||||
pysubs2 is a Python library for editing subtitle files.
|
||||
It’s based on *SubStation Alpha*, the native format of
|
||||
`Aegisub <http://www.aegisub.org/>`_; it also supports *SubRip (SRT)*,
|
||||
*MicroDVD*, *MPL2*, *TMP* and *WebVTT* formats and *OpenAI Whisper* captions.
|
||||
|
||||
There is a small CLI tool for batch conversion and retiming.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ pip install pysubs2
|
||||
$ pysubs2 --shift 0.3s *.srt
|
||||
$ pysubs2 --to srt *.ass
|
||||
|
||||
.. code:: python
|
||||
|
||||
import pysubs2
|
||||
subs = pysubs2.load("my_subtitles.ass", encoding="utf-8")
|
||||
subs.shift(s=2.5)
|
||||
for line in subs:
|
||||
line.text = "{\\be1}" + line.text
|
||||
subs.save("my_subtitles_edited.ass")
|
||||
|
||||
To learn more, please `see the documentation <http://pysubs2.readthedocs.io>`_.
|
||||
If you'd like to contribute, see `CONTRIBUTING.md <CONTRIBUTING.md>`_.
|
||||
|
||||
pysubs2 is licensed under the MIT license (see `LICENSE.txt <LICENSE.txt>`_).
|
|
@ -1,28 +0,0 @@
|
|||
../../bin/pysubs2,sha256=1ipjtihceH0M9ljChd41YMd3co9-Mn5sngcbA3Kgupk,241
|
||||
pysubs2-1.6.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
pysubs2-1.6.1.dist-info/LICENSE.txt,sha256=Syr1zDKD8s_Wl4E6er5xVmuOZ1bvaYyLOxcEI7lW4KU,1063
|
||||
pysubs2-1.6.1.dist-info/METADATA,sha256=f8UZ93EkW3RJVHEXKuIX29IT8Cuz2siGq1IlcLS17tc,3281
|
||||
pysubs2-1.6.1.dist-info/RECORD,,
|
||||
pysubs2-1.6.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
pysubs2-1.6.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
||||
pysubs2-1.6.1.dist-info/entry_points.txt,sha256=mznmeaia9Ec19KsaIM8D4tt6V0HnOUlPhSg5nXiAfAE,49
|
||||
pysubs2-1.6.1.dist-info/top_level.txt,sha256=i_V6JX-AWjMX15-Dzyc0RlrInU7Zun9PYfdB9brM95E,8
|
||||
pysubs2/__init__.py,sha256=lkW5MzctUNvTu5KPVr84eO3Nh2x5yHp0SWAJ4NnAp-Y,505
|
||||
pysubs2/__main__.py,sha256=3iAK73rLxjLJqFMYD-jn3PDJvHZ8rIOW_NgsmBkRGhA,134
|
||||
pysubs2/cli.py,sha256=6zWUOifOCm5ziwn0dd_MsxZfdFcLiaGpnCvbz65WqbA,10607
|
||||
pysubs2/common.py,sha256=SWYT6_aoL3CBEO-j0vP2EZnTDcUvNom6hW_y8gChGFM,1479
|
||||
pysubs2/exceptions.py,sha256=pEbAUuDj3GOUbxryLAdZa7qawMWxNhIGyzC3j-VCykM,633
|
||||
pysubs2/formatbase.py,sha256=WLHV7ppth7giTxqjHN13exyx1tfuwJGpEq0iNCMGYUY,2945
|
||||
pysubs2/formats.py,sha256=nlGNc9dnC1vHQF-qAZiQEqfBN7Mn0B8OPGCr_ezzPvc,2438
|
||||
pysubs2/jsonformat.py,sha256=Z7BfaqGkK4vl0Gc9lPF_UUPUi3l4oJlxD-0TOSLDt9w,1862
|
||||
pysubs2/microdvd.py,sha256=boTIasQUUV8430k5kz8ba2GgCGqr1hGEEbdlE7Lw7dI,4398
|
||||
pysubs2/mpl2.py,sha256=T1JUIAG3zCJHKoLRlYVLI_qFbvdkgV0w6XG9wlYJ17Q,1802
|
||||
pysubs2/ssaevent.py,sha256=bz5oGH2yIJQl7Lzi4Dw_KFBfLMPelmomxXAeNJ6tXTc,5883
|
||||
pysubs2/ssafile.py,sha256=b9adFdbx1MdHEB_ZdRon7GwKT5pKsaJhnnTE7UHcaKM,20356
|
||||
pysubs2/ssastyle.py,sha256=PFFejFEwaUx_bO1lkoHVSSJcPhupGO3epCCBCAEgclk,3995
|
||||
pysubs2/subrip.py,sha256=lK8bxf98wLKqordXeY2j8DwknDBN8kyLZXVFbZwj3mw,7371
|
||||
pysubs2/substation.py,sha256=xUC19ASpj3yDS9ASzFjNn88KnIqkvWqE_vPOVxWCKsM,16170
|
||||
pysubs2/time.py,sha256=1GPQlajjEio9rJvGnNO8J5OnWChQlB3BgAzRS3gnEzM,4641
|
||||
pysubs2/tmp.py,sha256=hJ4hpja7AckpGmdbC5KuZ2jyIEXWu2b1_SILqzbR5iQ,3954
|
||||
pysubs2/webvtt.py,sha256=sNFtGAaWn7gVMG8SVHS0Ndcku6LyiAaiv9gPErl0kys,1531
|
||||
pysubs2/whisper.py,sha256=5dywTdlXa46ulYbVeWpM_dH9jiwRKlTm6G1GrzIjsx0,1604
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2014-2023 Tomas Karabela
|
||||
Copyright (c) 2014-2024 Tomas Karabela
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
70
libs/pysubs2-1.7.2.dist-info/METADATA
Normal file
70
libs/pysubs2-1.7.2.dist-info/METADATA
Normal file
|
@ -0,0 +1,70 @@
|
|||
Metadata-Version: 2.1
|
||||
Name: pysubs2
|
||||
Version: 1.7.2
|
||||
Summary: A library for editing subtitle files
|
||||
Home-page: https://github.com/tkarabela/pysubs2
|
||||
Author: Tomas Karabela
|
||||
Author-email: tkarabela@seznam.cz
|
||||
License: MIT
|
||||
Project-URL: Documentation, https://pysubs2.readthedocs.io
|
||||
Project-URL: Bug Tracker, https://github.com/tkarabela/pysubs2/issues
|
||||
Keywords: SubStation,SubRip,MicroDVD,MPL2,WebVTT,ass,srt,sub,vtt,subtitles,captions
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Classifier: Topic :: Text Processing :: Markup
|
||||
Classifier: Topic :: Multimedia :: Video
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Intended Audience :: End Users/Desktop
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE.txt
|
||||
|
||||
pysubs2
|
||||
=======
|
||||
|
||||
[![pysubs2 build master branch](https://img.shields.io/github/actions/workflow/status/tkarabela/pysubs2/main.yml?branch=master)](https://github.com/tkarabela/pysubs2/actions)
|
||||
[![pysubs2 test code coverage](https://img.shields.io/codecov/c/github/tkarabela/pysubs2)](https://app.codecov.io/github/tkarabela/pysubs2)
|
||||
[![Static Badge](https://img.shields.io/badge/MyPy%20%26%20Ruffle-checked-blue?style=flat)](https://github.com/tkarabela/pysubs2/actions)
|
||||
[![PyPI - Version](https://img.shields.io/pypi/v/pysubs2.svg?style=flat)](https://pypi.org/project/pysubs2/)
|
||||
[![PyPI - Status](https://img.shields.io/pypi/status/pysubs2.svg?style=flat)](https://pypi.org/project/pysubs2/)
|
||||
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pysubs2.svg?style=flat)](https://pypi.org/project/pysubs2/)
|
||||
[![PyPI - License](https://img.shields.io/pypi/l/pysubs2.svg?style=flat)](LICENSE.txt)
|
||||
[![GitHub Repo stars](https://img.shields.io/github/stars/tkarabela/pysubs2?style=flat&label=GitHub%20stars)](https://github.com/tkarabela/pysubs2)
|
||||
|
||||
|
||||
pysubs2 is a Python library for editing subtitle files.
|
||||
It’s based on *SubStation Alpha*, the native format of
|
||||
[Aegisub](http://www.aegisub.org/); it also supports *SubRip (SRT)*,
|
||||
*MicroDVD*, *MPL2*, *TMP* and *WebVTT* formats and *OpenAI Whisper* captions.
|
||||
|
||||
There is a small CLI tool for batch conversion and retiming.
|
||||
|
||||
```bash
|
||||
pip install pysubs2
|
||||
pysubs2 --shift 0.3s *.srt
|
||||
pysubs2 --to srt *.ass
|
||||
```
|
||||
|
||||
```python
|
||||
import pysubs2
|
||||
subs = pysubs2.load("my_subtitles.ass", encoding="utf-8")
|
||||
subs.shift(s=2.5)
|
||||
for line in subs:
|
||||
line.text = "{\\be1}" + line.text
|
||||
subs.save("my_subtitles_edited.ass")
|
||||
```
|
||||
|
||||
To learn more, please [see the documentation](http://pysubs2.readthedocs.io).
|
||||
If you'd like to contribute, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
pysubs2 is licensed under the MIT license (see [LICENSE.txt](LICENSE.txt)).
|
47
libs/pysubs2-1.7.2.dist-info/RECORD
Normal file
47
libs/pysubs2-1.7.2.dist-info/RECORD
Normal file
|
@ -0,0 +1,47 @@
|
|||
../../bin/pysubs2,sha256=qbyZPJ0JMXc2Ii34KZgXKNvu1v8En-GFSWQFwhstugg,249
|
||||
pysubs2-1.7.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
pysubs2-1.7.2.dist-info/LICENSE.txt,sha256=WYgskvBrUBBDxuKH9JSV1u22g-UBTwHjeuJLlPp6lWU,1063
|
||||
pysubs2-1.7.2.dist-info/METADATA,sha256=2Fm6UaI7kElE-smr83cNBIfWRXt0vfso1Kyx5sChjB0,3183
|
||||
pysubs2-1.7.2.dist-info/RECORD,,
|
||||
pysubs2-1.7.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
pysubs2-1.7.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
||||
pysubs2-1.7.2.dist-info/entry_points.txt,sha256=mznmeaia9Ec19KsaIM8D4tt6V0HnOUlPhSg5nXiAfAE,49
|
||||
pysubs2-1.7.2.dist-info/top_level.txt,sha256=i_V6JX-AWjMX15-Dzyc0RlrInU7Zun9PYfdB9brM95E,8
|
||||
pysubs2/__init__.py,sha256=L71MMMS6zcAiGWdddhnc4COZmKLYX__igP0Tr5q-y3Q,783
|
||||
pysubs2/__main__.py,sha256=3iAK73rLxjLJqFMYD-jn3PDJvHZ8rIOW_NgsmBkRGhA,134
|
||||
pysubs2/__pycache__/__init__.cpython-311.pyc,,
|
||||
pysubs2/__pycache__/__main__.cpython-311.pyc,,
|
||||
pysubs2/__pycache__/cli.cpython-311.pyc,,
|
||||
pysubs2/__pycache__/common.cpython-311.pyc,,
|
||||
pysubs2/__pycache__/exceptions.cpython-311.pyc,,
|
||||
pysubs2/__pycache__/ssaevent.cpython-311.pyc,,
|
||||
pysubs2/__pycache__/ssafile.cpython-311.pyc,,
|
||||
pysubs2/__pycache__/ssastyle.cpython-311.pyc,,
|
||||
pysubs2/__pycache__/time.cpython-311.pyc,,
|
||||
pysubs2/cli.py,sha256=lFVZsLChtnnaRov2lNEHlecTJaoNNvTLMQmZe0-4oGE,11916
|
||||
pysubs2/common.py,sha256=33XyqTSPqUNMedlnVkx-7GWFwvX0Fpce1G_ZHHtwQuI,1503
|
||||
pysubs2/exceptions.py,sha256=5XMoOvloP4QPPkEPour6YalxomcK-RkJA9eb_ecIbH0,2318
|
||||
pysubs2/formats/__init__.py,sha256=KZy9IibVmK-WwCHAYYplJELmYEnQu9BBRz5HEvyZ5U4,2515
|
||||
pysubs2/formats/__pycache__/__init__.cpython-311.pyc,,
|
||||
pysubs2/formats/__pycache__/base.cpython-311.pyc,,
|
||||
pysubs2/formats/__pycache__/jsonformat.cpython-311.pyc,,
|
||||
pysubs2/formats/__pycache__/microdvd.cpython-311.pyc,,
|
||||
pysubs2/formats/__pycache__/mpl2.cpython-311.pyc,,
|
||||
pysubs2/formats/__pycache__/subrip.cpython-311.pyc,,
|
||||
pysubs2/formats/__pycache__/substation.cpython-311.pyc,,
|
||||
pysubs2/formats/__pycache__/tmp.cpython-311.pyc,,
|
||||
pysubs2/formats/__pycache__/webvtt.cpython-311.pyc,,
|
||||
pysubs2/formats/__pycache__/whisper.cpython-311.pyc,,
|
||||
pysubs2/formats/base.py,sha256=zzt4L3KIC6VrUNfzwjDHSF5HV7swrdo65dop90vfy70,2959
|
||||
pysubs2/formats/jsonformat.py,sha256=TWk_qOI0-GaV_Jzs2S5JOe7uWu7t1jllO9NYOnZgRcE,2101
|
||||
pysubs2/formats/microdvd.py,sha256=hfshagBIMMPz3WgqpttYGugtAj_6CheAiTkvTTrn3NE,5970
|
||||
pysubs2/formats/mpl2.py,sha256=vpyL5j7gxxkGvWvI8JXwFlHaEyCBHYTn_mtgk4zqNFw,1970
|
||||
pysubs2/formats/subrip.py,sha256=VsEhj6aX_NZdUbecxjKk-5Ri7GcUdGh8dfzZVTsAZSM,7484
|
||||
pysubs2/formats/substation.py,sha256=UFaZ3sZoCa3bizKvn_5Ugtqy6crO1FqldxS5OI7ozcQ,16795
|
||||
pysubs2/formats/tmp.py,sha256=FKktjpgv07rf6EZ1Og0rkNpcj59Z6kL6dL-V0QLSKQw,4100
|
||||
pysubs2/formats/webvtt.py,sha256=Ycur_E5ztjazW2akyUXTsbNUdLs4S-knq93hwompCac,1788
|
||||
pysubs2/formats/whisper.py,sha256=JHGY8nhiVso_iyVZOP72lLbC7GM-gX_FFc1BdzCGdo0,1607
|
||||
pysubs2/ssaevent.py,sha256=VR0Pso779Pw5bs-Q-VpIz8Zs-A6p2a2pptQtsFVjRnM,6291
|
||||
pysubs2/ssafile.py,sha256=smMZIFk5h8n3uUuzgrcSmzdNsVU7oB3kguKQtUAdQGU,23833
|
||||
pysubs2/ssastyle.py,sha256=hlNowFmuiiwOzWTtQxKUakTwcE7Q5KxlQtl64Q6ScMU,4031
|
||||
pysubs2/time.py,sha256=Onk1vs9xXKiuwYK0RorZVU29p2FCEARapnfP_yYczTQ,4739
|
|
@ -1,5 +1,5 @@
|
|||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.42.0)
|
||||
Generator: bdist_wheel (0.43.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
|
@ -1,10 +1,28 @@
|
|||
from .ssafile import SSAFile
|
||||
from .ssaevent import SSAEvent
|
||||
from .ssastyle import SSAStyle
|
||||
from . import time, formats, cli, whisper
|
||||
from .exceptions import *
|
||||
from . import time, formats, cli, exceptions
|
||||
from .formats import whisper
|
||||
from .exceptions import * # noqa: F403
|
||||
from .common import Color, Alignment, VERSION
|
||||
|
||||
__all__ = [
|
||||
"SSAFile",
|
||||
"SSAEvent",
|
||||
"SSAStyle",
|
||||
"time",
|
||||
"formats",
|
||||
"cli",
|
||||
"whisper",
|
||||
"exceptions",
|
||||
"Color",
|
||||
"Alignment",
|
||||
"VERSION",
|
||||
"load",
|
||||
"load_from_whisper",
|
||||
"make_time",
|
||||
]
|
||||
|
||||
#: Alias for :meth:`SSAFile.load()`.
|
||||
load = SSAFile.load
|
||||
|
||||
|
|
|
@ -3,10 +3,11 @@ import codecs
|
|||
import os
|
||||
import re
|
||||
import os.path as op
|
||||
import io
|
||||
from io import open
|
||||
from io import TextIOWrapper
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
from typing import List
|
||||
|
||||
from .formats import get_file_extension, FORMAT_IDENTIFIERS
|
||||
from .time import make_time
|
||||
from .ssafile import SSAFile
|
||||
|
@ -42,7 +43,7 @@ def change_ext(path: str, ext: str) -> str:
|
|||
|
||||
|
||||
class Pysubs2CLI:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
parser = self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
prog="pysubs2",
|
||||
description=dedent("""
|
||||
|
@ -80,6 +81,12 @@ class Pysubs2CLI:
|
|||
"If you wish to convert between encodings, make sure --input-enc is set correctly! "
|
||||
"Otherwise, your output files will probably be corrupted. It's a good idea to "
|
||||
"back up your files or use the -o option.")
|
||||
parser.add_argument("--enc-error-handling", choices=("strict", "surrogateescape"),
|
||||
default="surrogateescape",
|
||||
help="Character encoding error handling for input and output. Defaults to 'surrogateescape' "
|
||||
"which passes through unrecognized characters to output unchanged. Use 'strict' if "
|
||||
"you want the command to fail when encountering a character incompatible with selected "
|
||||
"input/output encoding.")
|
||||
parser.add_argument("--fps", metavar="FPS", type=positive_float,
|
||||
help="This argument specifies framerate for MicroDVD files. By default, framerate "
|
||||
"is detected from the file. Use this when framerate specification is missing "
|
||||
|
@ -116,13 +123,14 @@ class Pysubs2CLI:
|
|||
extra_sub_options.add_argument("--sub-no-write-fps-declaration", action="store_true",
|
||||
help="(output) omit writing FPS as first zero-length subtitle")
|
||||
|
||||
def __call__(self, argv):
|
||||
def __call__(self, argv: List[str]) -> int:
|
||||
try:
|
||||
self.main(argv)
|
||||
return self.main(argv)
|
||||
except KeyboardInterrupt:
|
||||
exit("\nAborted by user.")
|
||||
print("\nAborted by user.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
def main(self, argv):
|
||||
def main(self, argv: List[str]) -> int:
|
||||
args = self.parser.parse_args(argv)
|
||||
errors = 0
|
||||
|
||||
|
@ -157,7 +165,7 @@ class Pysubs2CLI:
|
|||
print("Skipping", path, "(not a file)")
|
||||
errors += 1
|
||||
else:
|
||||
with open(path, encoding=args.input_enc) as infile:
|
||||
with open(path, encoding=args.input_enc, errors=args.enc_error_handling) as infile:
|
||||
subs = SSAFile.from_file(infile, args.input_format, args.fps, **extra_input_args)
|
||||
|
||||
self.process(subs, args)
|
||||
|
@ -165,31 +173,37 @@ class Pysubs2CLI:
|
|||
if args.output_format is None:
|
||||
outpath = path
|
||||
output_format = subs.format
|
||||
assert output_format is not None, "subs.format must not be None (it was read from file)"
|
||||
else:
|
||||
ext = get_file_extension(args.output_format)
|
||||
outpath = change_ext(path, ext)
|
||||
output_format = args.output_format
|
||||
assert output_format is not None, "args.output_format must not be None (see if/else)"
|
||||
|
||||
if args.output_dir is not None:
|
||||
_, filename = op.split(outpath)
|
||||
outpath = op.join(args.output_dir, filename)
|
||||
|
||||
with open(outpath, "w", encoding=args.output_enc) as outfile:
|
||||
with open(outpath, "w", encoding=args.output_enc, errors=args.enc_error_handling) as outfile:
|
||||
subs.to_file(outfile, output_format, args.fps, apply_styles=not args.clean,
|
||||
**extra_output_args)
|
||||
else:
|
||||
infile = io.TextIOWrapper(sys.stdin.buffer, args.input_enc)
|
||||
outfile = io.TextIOWrapper(sys.stdout.buffer, args.output_enc)
|
||||
elif not sys.stdin.isatty():
|
||||
infile = TextIOWrapper(sys.stdin.buffer, encoding=args.input_enc, errors=args.enc_error_handling)
|
||||
outfile = TextIOWrapper(sys.stdout.buffer, encoding=args.output_enc, errors=args.enc_error_handling)
|
||||
|
||||
subs = SSAFile.from_file(infile, args.input_format, args.fps)
|
||||
self.process(subs, args)
|
||||
output_format = args.output_format or subs.format
|
||||
assert output_format is not None, "output_format must not be None (it's either given or inferred at read time)"
|
||||
subs.to_file(outfile, output_format, args.fps, apply_styles=not args.clean)
|
||||
else:
|
||||
self.parser.print_help()
|
||||
errors += 1
|
||||
|
||||
return (0 if errors == 0 else 1)
|
||||
return 0 if errors == 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def process(subs, args):
|
||||
def process(subs: SSAFile, args: argparse.Namespace) -> None:
|
||||
if args.shift is not None:
|
||||
subs.shift(ms=args.shift)
|
||||
elif args.shift_back is not None:
|
||||
|
@ -202,7 +216,7 @@ class Pysubs2CLI:
|
|||
subs.remove_miscellaneous_events()
|
||||
|
||||
|
||||
def __main__():
|
||||
def __main__() -> None:
|
||||
cli = Pysubs2CLI()
|
||||
rv = cli(sys.argv[1:])
|
||||
sys.exit(rv)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
from typing import Tuple, Union
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
|
@ -54,11 +54,11 @@ class Alignment(IntEnum):
|
|||
return SSA_ALIGNMENT[self.value - 1]
|
||||
|
||||
|
||||
SSA_ALIGNMENT = (1, 2, 3, 9, 10, 11, 5, 6, 7)
|
||||
SSA_ALIGNMENT: Tuple[int, ...] = (1, 2, 3, 9, 10, 11, 5, 6, 7)
|
||||
|
||||
|
||||
#: Version of the pysubs2 library.
|
||||
VERSION = "1.6.1"
|
||||
VERSION = "1.7.2"
|
||||
|
||||
|
||||
IntOrFloat = Union[int, float]
|
||||
|
|
|
@ -1,3 +1,15 @@
|
|||
from typing import List
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Pysubs2Error",
|
||||
"UnknownFPSError",
|
||||
"UnknownFileExtensionError",
|
||||
"UnknownFormatIdentifierError",
|
||||
"FormatAutodetectionError",
|
||||
]
|
||||
|
||||
|
||||
class Pysubs2Error(Exception):
|
||||
"""Base class for pysubs2 exceptions."""
|
||||
|
||||
|
@ -7,16 +19,58 @@ class UnknownFPSError(Pysubs2Error):
|
|||
|
||||
|
||||
class UnknownFileExtensionError(Pysubs2Error):
|
||||
"""File extension does not pertain to any known subtitle format."""
|
||||
"""
|
||||
File extension does not pertain to any known subtitle format.
|
||||
|
||||
This exception is raised by `SSAFile.save()` when the ``format_`` parameter
|
||||
is not specified. It will try to guess the desired format from output filename
|
||||
and raise this exception when it fails.
|
||||
|
||||
Attributes:
|
||||
ext (str): File extension
|
||||
"""
|
||||
|
||||
def __init__(self, ext: str) -> None:
|
||||
self.ext = ext
|
||||
msg = f"File extension {ext!r} does not match any supported subtitle format"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class UnknownFormatIdentifierError(Pysubs2Error):
|
||||
"""Unknown subtitle format identifier (ie. string like ``"srt"``)."""
|
||||
"""
|
||||
Unknown subtitle format identifier (ie. string like ``"srt"``).
|
||||
|
||||
This exception is used when interpreting ``format_`` parameter fails,
|
||||
eg. in `SSAFile.save()`.
|
||||
|
||||
Attributes:
|
||||
format_ (str): Format identifier
|
||||
"""
|
||||
|
||||
def __init__(self, format_: str) -> None:
|
||||
self.format_ = format_
|
||||
msg = f"Format identifier {format_!r} does not match any supported subtitle format"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class FormatAutodetectionError(Pysubs2Error):
|
||||
"""Subtitle format is ambiguous or unknown."""
|
||||
"""
|
||||
Subtitle format is ambiguous or unknown based on analysis of file fragment
|
||||
|
||||
This exception is raised by `SSAFile.load()` and related methods
|
||||
when the ``format_`` parameter is not specified. It will try to guess
|
||||
the input format based on reading first few kilobytes of the input file
|
||||
and raise this exception if the format cannot be uniquely determined.
|
||||
|
||||
class ContentNotUsable(Pysubs2Error):
|
||||
"""Current content not usable for specified format"""
|
||||
Attributes:
|
||||
content (str): Analyzed subtitle file content
|
||||
formats (list[str]): Format identifiers for detected formats
|
||||
"""
|
||||
def __init__(self, content: str, formats: List[str]) -> None:
|
||||
self.content = content
|
||||
self.formats = formats
|
||||
if not formats:
|
||||
msg = "No suitable formats"
|
||||
else:
|
||||
msg = f"Multiple suitable formats ({formats!r})"
|
||||
super().__init__(msg)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from typing import Dict, Type
|
||||
|
||||
from .formatbase import FormatBase
|
||||
from .base import FormatBase
|
||||
from .microdvd import MicroDVDFormat
|
||||
from .subrip import SubripFormat
|
||||
from .jsonformat import JSONFormat
|
||||
|
@ -8,7 +8,7 @@ from .substation import SubstationFormat
|
|||
from .mpl2 import MPL2Format
|
||||
from .tmp import TmpFormat
|
||||
from .webvtt import WebVTTFormat
|
||||
from .exceptions import *
|
||||
from ..exceptions import UnknownFormatIdentifierError, UnknownFileExtensionError, FormatAutodetectionError
|
||||
|
||||
#: Dict mapping file extensions to format identifiers.
|
||||
FILE_EXTENSION_TO_FORMAT_IDENTIFIER: Dict[str, str] = {
|
||||
|
@ -75,6 +75,6 @@ def autodetect_format(content: str) -> str:
|
|||
if len(formats) == 1:
|
||||
return formats.pop()
|
||||
elif not formats:
|
||||
raise FormatAutodetectionError("No suitable formats")
|
||||
raise FormatAutodetectionError(content=content, formats=[])
|
||||
else:
|
||||
raise FormatAutodetectionError(f"Multiple suitable formats ({formats!r})")
|
||||
raise FormatAutodetectionError(content=content, formats=list(formats))
|
|
@ -1,6 +1,5 @@
|
|||
from typing import Optional
|
||||
import io
|
||||
import pysubs2
|
||||
from typing import Optional, Any, TextIO
|
||||
from ..ssafile import SSAFile
|
||||
|
||||
|
||||
class FormatBase:
|
||||
|
@ -19,7 +18,7 @@ class FormatBase:
|
|||
|
||||
"""
|
||||
@classmethod
|
||||
def from_file(cls, subs: "pysubs2.SSAFile", fp: io.TextIOBase, format_: str, **kwargs):
|
||||
def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None:
|
||||
"""
|
||||
Load subtitle file into an empty SSAFile.
|
||||
|
||||
|
@ -42,7 +41,7 @@ class FormatBase:
|
|||
raise NotImplementedError("Parsing is not supported for this format")
|
||||
|
||||
@classmethod
|
||||
def to_file(cls, subs: "pysubs2.SSAFile", fp: io.TextIOBase, format_: str, **kwargs):
|
||||
def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None:
|
||||
"""
|
||||
Write SSAFile into a file.
|
||||
|
|
@ -1,15 +1,18 @@
|
|||
import dataclasses
|
||||
import json
|
||||
from .common import Color
|
||||
from .ssaevent import SSAEvent
|
||||
from .ssastyle import SSAStyle
|
||||
from .formatbase import FormatBase
|
||||
from typing import Any, Optional, TextIO
|
||||
|
||||
from ..common import Color
|
||||
from ..ssaevent import SSAEvent
|
||||
from ..ssastyle import SSAStyle
|
||||
from .base import FormatBase
|
||||
from ..ssafile import SSAFile
|
||||
|
||||
|
||||
# We're using Color dataclass
|
||||
# https://stackoverflow.com/questions/51286748/make-the-python-json-encoder-support-pythons-new-dataclasses
|
||||
class EnhancedJSONEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
def default(self, o: Any) -> Any:
|
||||
if dataclasses.is_dataclass(o):
|
||||
return dataclasses.asdict(o)
|
||||
return super().default(o)
|
||||
|
@ -22,13 +25,15 @@ class JSONFormat(FormatBase):
|
|||
This is essentially SubStation Alpha as JSON.
|
||||
"""
|
||||
@classmethod
|
||||
def guess_format(cls, text):
|
||||
def guess_format(cls, text: str) -> Optional[str]:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.guess_format()`"""
|
||||
if text.startswith("{\""):
|
||||
if text.startswith("{\"") and "\"info\":" in text:
|
||||
return "json"
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, subs, fp, format_, **kwargs):
|
||||
def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.from_file()`"""
|
||||
data = json.load(fp)
|
||||
|
||||
|
@ -47,7 +52,7 @@ class JSONFormat(FormatBase):
|
|||
subs.events = [SSAEvent(**fields) for fields in data["events"]]
|
||||
|
||||
@classmethod
|
||||
def to_file(cls, subs, fp, format_, **kwargs):
|
||||
def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.to_file()`"""
|
||||
data = {
|
||||
"info": dict(**subs.info),
|
|
@ -1,11 +1,15 @@
|
|||
from functools import partial
|
||||
import re
|
||||
from .exceptions import UnknownFPSError
|
||||
from .ssaevent import SSAEvent
|
||||
from .ssastyle import SSAStyle
|
||||
from .formatbase import FormatBase
|
||||
from typing import Optional, TextIO, Any, Match
|
||||
|
||||
from ..exceptions import UnknownFPSError
|
||||
from ..ssaevent import SSAEvent
|
||||
from ..ssastyle import SSAStyle
|
||||
from .base import FormatBase
|
||||
from .substation import parse_tags
|
||||
from .time import ms_to_frames, frames_to_ms
|
||||
from ..time import ms_to_frames, frames_to_ms
|
||||
from ..ssafile import SSAFile
|
||||
|
||||
|
||||
#: Matches a MicroDVD line.
|
||||
MICRODVD_LINE = re.compile(r" *\{ *(\d+) *\} *\{ *(\d+) *\}(.+)")
|
||||
|
@ -14,14 +18,33 @@ MICRODVD_LINE = re.compile(r" *\{ *(\d+) *\} *\{ *(\d+) *\}(.+)")
|
|||
class MicroDVDFormat(FormatBase):
|
||||
"""MicroDVD subtitle format implementation"""
|
||||
@classmethod
|
||||
def guess_format(cls, text):
|
||||
def guess_format(cls, text: str) -> Optional[str]:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.guess_format()`"""
|
||||
if any(map(MICRODVD_LINE.match, text.splitlines())):
|
||||
return "microdvd"
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, subs, fp, format_, fps=None, **kwargs):
|
||||
"""See :meth:`pysubs2.formats.FormatBase.from_file()`"""
|
||||
def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, fps: Optional[float] = None,
|
||||
strict_fps_inference: bool = True, **kwargs: Any) -> None:
|
||||
"""
|
||||
See :meth:`pysubs2.formats.FormatBase.from_file()`
|
||||
|
||||
Keyword args:
|
||||
strict_fps_inference: If True (default), in the case when ``fps`` is not given, it will be read
|
||||
from the first subtitle text only if the start and end frame of this subtitle is ``{1}{1}``
|
||||
(matches VLC Player behaviour), otherwise :class:`pysubs2.exceptions.UnknownFPSError` is raised.
|
||||
|
||||
When ``strict_fps_inference``
|
||||
is False, framerate will be read from the first subtitle text in this case regardless of
|
||||
start and end frame (which may result in bogus result, if the first subtitle is not supposed
|
||||
to contain framerate). Before introduction of this option, the library behaved as if this
|
||||
option was False.
|
||||
|
||||
.. versionchanged:: 1.7.0
|
||||
Added the ``strict_fps_inference`` option.
|
||||
"""
|
||||
for line in fp:
|
||||
match = MICRODVD_LINE.match(line)
|
||||
if not match:
|
||||
|
@ -31,10 +54,14 @@ class MicroDVDFormat(FormatBase):
|
|||
fstart, fend = map(int, (fstart, fend))
|
||||
|
||||
if fps is None:
|
||||
# We don't know the framerate, but it is customary to include
|
||||
# it as text of the first subtitle. In that case, we skip
|
||||
# this auxiliary subtitle and proceed with reading.
|
||||
# We don't know the framerate, but it is customary to include it as text of the first subtitle,
|
||||
# in the format {1}{1}fps, see pysubs2 issue #71 or VLC player source:
|
||||
# https://code.videolan.org/videolan/vlc/-/blob/dccda0e133ff0a2e85de727cf19ddbc634f06b67/modules/demux/subtitle.c#L1014
|
||||
# In that case, we skip this auxiliary subtitle and proceed with reading.
|
||||
try:
|
||||
if strict_fps_inference and not (fstart == 1 and fend == 1):
|
||||
raise ValueError("Frame mismatch, expected {1}{1}")
|
||||
|
||||
fps = float(text)
|
||||
subs.fps = fps
|
||||
continue
|
||||
|
@ -45,10 +72,10 @@ class MicroDVDFormat(FormatBase):
|
|||
|
||||
start, end = map(partial(frames_to_ms, fps=fps), (fstart, fend))
|
||||
|
||||
def prepare_text(text):
|
||||
def prepare_text(text: str) -> str:
|
||||
text = text.replace("|", r"\N")
|
||||
|
||||
def style_replacer(match: re.Match) -> str:
|
||||
def style_replacer(match: Match[str]) -> str:
|
||||
tags = [c for c in "biu" if c in match.group(0)]
|
||||
return "{%s}" % "".join(f"\\{c}1" for c in tags)
|
||||
|
||||
|
@ -63,14 +90,15 @@ class MicroDVDFormat(FormatBase):
|
|||
subs.append(ev)
|
||||
|
||||
@classmethod
|
||||
def to_file(cls, subs, fp, format_, fps=None, write_fps_declaration=True, apply_styles=True, **kwargs):
|
||||
def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, fps: Optional[float] = None,
|
||||
write_fps_declaration: bool = True, apply_styles: bool = True, **kwargs: Any) -> None:
|
||||
"""
|
||||
See :meth:`pysubs2.formats.FormatBase.to_file()`
|
||||
|
||||
The only supported styling is marking whole lines italic.
|
||||
|
||||
Keyword args:
|
||||
write_fps_declaration: If True, create a zero-duration first subtitle which will contain
|
||||
write_fps_declaration: If True, create a zero-duration first subtitle ``{1}{1}`` which will contain
|
||||
the fps.
|
||||
apply_styles: If False, do not write any styling.
|
||||
|
||||
|
@ -94,12 +122,9 @@ class MicroDVDFormat(FormatBase):
|
|||
|
||||
# insert an artificial first line telling the framerate
|
||||
if write_fps_declaration:
|
||||
subs.insert(0, SSAEvent(start=0, end=0, text=str(fps)))
|
||||
|
||||
for line in subs:
|
||||
if line.is_comment or line.is_drawing:
|
||||
continue
|
||||
subs.insert(0, SSAEvent(start=1, end=1, text=str(fps)))
|
||||
|
||||
for line in subs.get_text_events():
|
||||
text = "|".join(line.plaintext.splitlines())
|
||||
if apply_styles and is_entirely_italic(line):
|
||||
text = "{Y:i}" + text
|
||||
|
@ -107,8 +132,10 @@ class MicroDVDFormat(FormatBase):
|
|||
start, end = map(to_frames, (line.start, line.end))
|
||||
|
||||
# XXX warn on underflow?
|
||||
if start < 0: start = 0
|
||||
if end < 0: end = 0
|
||||
if start < 0:
|
||||
start = 0
|
||||
if end < 0:
|
||||
end = 0
|
||||
|
||||
print("{%d}{%d}%s" % (start, end, text), file=fp)
|
||||
|
60
libs/pysubs2/formats/mpl2.py
Normal file
60
libs/pysubs2/formats/mpl2.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
import re
|
||||
from typing import Optional, Any, TextIO
|
||||
from ..time import times_to_ms
|
||||
from .base import FormatBase
|
||||
from ..ssaevent import SSAEvent
|
||||
from ..ssafile import SSAFile
|
||||
|
||||
|
||||
# thanks to http://otsaloma.io/gaupol/doc/api/aeidon.files.mpl2_source.html
|
||||
MPL2_FORMAT = re.compile(r"^\[(-?\d+)\]\[(-?\d+)\](.*)", re.MULTILINE)
|
||||
|
||||
|
||||
class MPL2Format(FormatBase):
|
||||
"""MPL2 subtitle format implementation"""
|
||||
@classmethod
|
||||
def guess_format(cls, text: str) -> Optional[str]:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.guess_format()`"""
|
||||
if MPL2_FORMAT.search(text):
|
||||
return "mpl2"
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.from_file()`"""
|
||||
def prepare_text(lines: str) -> str:
|
||||
out = []
|
||||
for s in lines.split("|"):
|
||||
s = s.strip()
|
||||
|
||||
if s.startswith("/"):
|
||||
# line beginning with '/' is in italics
|
||||
s = r"{\i1}%s{\i0}" % s[1:].strip()
|
||||
|
||||
out.append(s)
|
||||
return "\\N".join(out)
|
||||
|
||||
text = fp.read()
|
||||
for start, end, text in MPL2_FORMAT.findall(text):
|
||||
e = SSAEvent(
|
||||
start=times_to_ms(s=float(start) / 10),
|
||||
end=times_to_ms(s=float(end) / 10),
|
||||
text=prepare_text(text)
|
||||
)
|
||||
subs.append(e)
|
||||
|
||||
@classmethod
|
||||
def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None:
|
||||
"""
|
||||
See :meth:`pysubs2.formats.FormatBase.to_file()`
|
||||
|
||||
No styling is supported at the moment.
|
||||
|
||||
"""
|
||||
# TODO handle italics
|
||||
for line in subs.get_text_events():
|
||||
start = int(line.start // 100)
|
||||
end = int(line.end // 100)
|
||||
text = line.plaintext.replace("\n", "|")
|
||||
print(f"[{start}][{end}] {text}", file=fp)
|
|
@ -1,14 +1,14 @@
|
|||
import re
|
||||
import warnings
|
||||
from typing import List
|
||||
from typing import List, Sequence, Optional, TextIO, Any, Tuple
|
||||
|
||||
import pysubs2
|
||||
from .formatbase import FormatBase
|
||||
from .ssaevent import SSAEvent
|
||||
from .ssastyle import SSAStyle
|
||||
from .base import FormatBase
|
||||
from ..ssaevent import SSAEvent
|
||||
from ..ssastyle import SSAStyle
|
||||
from .substation import parse_tags
|
||||
from .exceptions import ContentNotUsable
|
||||
from .time import ms_to_times, make_time, TIMESTAMP, timestamp_to_ms
|
||||
from ..time import ms_to_times, make_time, TIMESTAMP, timestamp_to_ms
|
||||
from ..ssafile import SSAFile
|
||||
|
||||
|
||||
#: Largest timestamp allowed in SubRip, ie. 99:59:59,999.
|
||||
MAX_REPRESENTABLE_TIME = make_time(h=100) - 1
|
||||
|
@ -30,11 +30,11 @@ class SubripFormat(FormatBase):
|
|||
return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
|
||||
|
||||
@staticmethod
|
||||
def timestamp_to_ms(groups):
|
||||
def timestamp_to_ms(groups: Sequence[str]) -> int:
|
||||
return timestamp_to_ms(groups)
|
||||
|
||||
@classmethod
|
||||
def guess_format(cls, text):
|
||||
def guess_format(cls, text: str) -> Optional[str]:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.guess_format()`"""
|
||||
if "[Script Info]" in text or "[V4+ Styles]" in text:
|
||||
# disambiguation vs. SSA/ASS
|
||||
|
@ -48,8 +48,11 @@ class SubripFormat(FormatBase):
|
|||
if len(cls.TIMESTAMP.findall(line)) == 2:
|
||||
return "srt"
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, subs, fp, format_, keep_html_tags=False, keep_unknown_html_tags=False, **kwargs):
|
||||
def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, keep_html_tags: bool = False,
|
||||
keep_unknown_html_tags: bool = False, **kwargs: Any) -> None:
|
||||
"""
|
||||
See :meth:`pysubs2.formats.FormatBase.from_file()`
|
||||
|
||||
|
@ -70,8 +73,8 @@ class SubripFormat(FormatBase):
|
|||
If False, these other HTML tags will be stripped from output
|
||||
(in the previous example, you would get only ``example {\\i1}text{\\i0}``).
|
||||
"""
|
||||
timestamps = [] # (start, end)
|
||||
following_lines = [] # contains lists of lines following each timestamp
|
||||
timestamps: List[Tuple[int, int]] = [] # (start, end)
|
||||
following_lines: List[List[str]] = [] # contains lists of lines following each timestamp
|
||||
|
||||
for line in fp:
|
||||
stamps = cls.TIMESTAMP.findall(line)
|
||||
|
@ -83,7 +86,7 @@ class SubripFormat(FormatBase):
|
|||
if timestamps:
|
||||
following_lines[-1].append(line)
|
||||
|
||||
def prepare_text(lines):
|
||||
def prepare_text(lines: List[str]) -> str:
|
||||
# Handle the "happy" empty subtitle case, which is timestamp line followed by blank line(s)
|
||||
# followed by number line and timestamp line of the next subtitle. Fixes issue #11.
|
||||
if (len(lines) >= 2
|
||||
|
@ -108,11 +111,13 @@ class SubripFormat(FormatBase):
|
|||
s = re.sub(r"\n", r"\\N", s) # convert newlines
|
||||
return s
|
||||
|
||||
subs.events = [SSAEvent(start=start, end=end, text=prepare_text(lines))
|
||||
for (start, end), lines in zip(timestamps, following_lines)]
|
||||
for (start, end), lines in zip(timestamps, following_lines):
|
||||
e = SSAEvent(start=start, end=end, text=prepare_text(lines))
|
||||
subs.append(e)
|
||||
|
||||
@classmethod
|
||||
def to_file(cls, subs, fp, format_, apply_styles=True, keep_ssa_tags=False, **kwargs):
|
||||
def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, apply_styles: bool = True,
|
||||
keep_ssa_tags: bool = False, **kwargs: Any) -> None:
|
||||
"""
|
||||
See :meth:`pysubs2.formats.FormatBase.to_file()`
|
||||
|
||||
|
@ -134,7 +139,7 @@ class SubripFormat(FormatBase):
|
|||
is SRT which doesn't use line styles - this shouldn't be much
|
||||
of an issue in practice.)
|
||||
"""
|
||||
def prepare_text(text: str, style: SSAStyle):
|
||||
def prepare_text(text: str, style: SSAStyle) -> str:
|
||||
text = text.replace(r"\h", " ")
|
||||
text = text.replace(r"\n", "\n")
|
||||
text = text.replace(r"\N", "\n")
|
||||
|
@ -145,24 +150,20 @@ class SubripFormat(FormatBase):
|
|||
else:
|
||||
for fragment, sty in parse_tags(text, style, subs.styles):
|
||||
if apply_styles:
|
||||
if sty.italic: fragment = f"<i>{fragment}</i>"
|
||||
if sty.underline: fragment = f"<u>{fragment}</u>"
|
||||
if sty.strikeout: fragment = f"<s>{fragment}</s>"
|
||||
if sty.drawing: raise ContentNotUsable
|
||||
if sty.italic:
|
||||
fragment = f"<i>{fragment}</i>"
|
||||
if sty.underline:
|
||||
fragment = f"<u>{fragment}</u>"
|
||||
if sty.strikeout:
|
||||
fragment = f"<s>{fragment}</s>"
|
||||
body.append(fragment)
|
||||
|
||||
return re.sub("\n+", "\n", "".join(body).strip())
|
||||
|
||||
visible_lines = cls._get_visible_lines(subs)
|
||||
|
||||
lineno = 1
|
||||
for line in visible_lines:
|
||||
for lineno, line in enumerate(cls._get_visible_lines(subs), 1):
|
||||
start = cls.ms_to_timestamp(line.start)
|
||||
end = cls.ms_to_timestamp(line.end)
|
||||
try:
|
||||
text = prepare_text(line.text, subs.styles.get(line.style, SSAStyle.DEFAULT_STYLE))
|
||||
except ContentNotUsable:
|
||||
continue
|
||||
text = prepare_text(line.text, subs.styles.get(line.style, SSAStyle.DEFAULT_STYLE))
|
||||
|
||||
print(lineno, file=fp)
|
||||
print(start, "-->", end, file=fp)
|
||||
|
@ -170,6 +171,5 @@ class SubripFormat(FormatBase):
|
|||
lineno += 1
|
||||
|
||||
@classmethod
|
||||
def _get_visible_lines(cls, subs: "pysubs2.SSAFile") -> List["pysubs2.SSAEvent"]:
|
||||
visible_lines = [line for line in subs if not line.is_comment]
|
||||
return visible_lines
|
||||
def _get_visible_lines(cls, subs: "SSAFile") -> List[SSAEvent]:
|
||||
return subs.get_text_events()
|
|
@ -1,22 +1,21 @@
|
|||
import logging
|
||||
import re
|
||||
import warnings
|
||||
from numbers import Number
|
||||
from typing import Any, Union, Optional, Dict
|
||||
from typing import Any, Union, Optional, Dict, Tuple, List, TextIO
|
||||
|
||||
import pysubs2
|
||||
from .formatbase import FormatBase
|
||||
from .ssaevent import SSAEvent
|
||||
from .ssastyle import SSAStyle
|
||||
from .common import Color, Alignment, SSA_ALIGNMENT
|
||||
from .time import make_time, ms_to_times, timestamp_to_ms, TIMESTAMP, TIMESTAMP_SHORT
|
||||
from .base import FormatBase
|
||||
from ..ssaevent import SSAEvent
|
||||
from ..ssastyle import SSAStyle
|
||||
from ..common import Color, Alignment, SSA_ALIGNMENT
|
||||
from ..time import make_time, ms_to_times, timestamp_to_ms, TIMESTAMP, TIMESTAMP_SHORT
|
||||
from ..ssafile import SSAFile
|
||||
|
||||
|
||||
def ass_to_ssa_alignment(i):
|
||||
def ass_to_ssa_alignment(i: int) -> int:
|
||||
warnings.warn("ass_to_ssa_alignment function is deprecated, please use the Alignment enum", DeprecationWarning)
|
||||
return SSA_ALIGNMENT[i-1]
|
||||
|
||||
def ssa_to_ass_alignment(i):
|
||||
def ssa_to_ass_alignment(i: int) -> int:
|
||||
warnings.warn("ssa_to_ass_alignment function is deprecated, please use the Alignment enum", DeprecationWarning)
|
||||
return SSA_ALIGNMENT.index(i) + 1
|
||||
|
||||
|
@ -86,7 +85,8 @@ def is_valid_field_content(s: str) -> bool:
|
|||
return "\n" not in s and "," not in s
|
||||
|
||||
|
||||
def parse_tags(text: str, style: SSAStyle = SSAStyle.DEFAULT_STYLE, styles: Optional[Dict[str, SSAStyle]] = None):
|
||||
def parse_tags(text: str, style: SSAStyle = SSAStyle.DEFAULT_STYLE,
|
||||
styles: Optional[Dict[str, SSAStyle]] = None) -> List[Tuple[str, SSAStyle]]:
|
||||
"""
|
||||
Split text into fragments with computed SSAStyles.
|
||||
|
||||
|
@ -117,14 +117,18 @@ def parse_tags(text: str, style: SSAStyle = SSAStyle.DEFAULT_STYLE, styles: Opti
|
|||
s = style.copy() # reset to original line style
|
||||
elif tag.startswith(r"\r"):
|
||||
name = tag[2:]
|
||||
if name in styles: # type: ignore[operator]
|
||||
if name in styles:
|
||||
# reset to named style
|
||||
s = styles[name].copy() # type: ignore[index]
|
||||
s = styles[name].copy()
|
||||
else:
|
||||
if "i" in tag: s.italic = "1" in tag
|
||||
elif "b" in tag: s.bold = "1" in tag
|
||||
elif "u" in tag: s.underline = "1" in tag
|
||||
elif "s" in tag: s.strikeout = "1" in tag
|
||||
if "i" in tag:
|
||||
s.italic = "1" in tag
|
||||
elif "b" in tag:
|
||||
s.bold = "1" in tag
|
||||
elif "u" in tag:
|
||||
s.underline = "1" in tag
|
||||
elif "s" in tag:
|
||||
s.strikeout = "1" in tag
|
||||
elif "p" in tag:
|
||||
try:
|
||||
scale = int(tag[2:])
|
||||
|
@ -146,34 +150,36 @@ class SubstationFormat(FormatBase):
|
|||
"""SubStation Alpha (ASS, SSA) subtitle format implementation"""
|
||||
|
||||
@staticmethod
|
||||
def ms_to_timestamp(ms: int) -> str:
|
||||
def ms_to_timestamp(requested_ms: int) -> str:
|
||||
"""Convert ms to 'H:MM:SS.cc'"""
|
||||
if ms < 0:
|
||||
ms = 0
|
||||
if ms > MAX_REPRESENTABLE_TIME:
|
||||
if requested_ms < 0:
|
||||
requested_ms = 0
|
||||
if requested_ms > MAX_REPRESENTABLE_TIME:
|
||||
warnings.warn("Overflow in SubStation timestamp, clamping to MAX_REPRESENTABLE_TIME", RuntimeWarning)
|
||||
ms = MAX_REPRESENTABLE_TIME
|
||||
|
||||
h, m, s, ms = ms_to_times(ms)
|
||||
requested_ms = MAX_REPRESENTABLE_TIME
|
||||
|
||||
# Aegisub does rounding, see https://github.com/Aegisub/Aegisub/blob/6f546951b4f004da16ce19ba638bf3eedefb9f31/libaegisub/include/libaegisub/ass/time.h#L32
|
||||
cs = ((ms + 5) - (ms + 5) % 10) // 10
|
||||
round_ms = ((requested_ms + 5) - (requested_ms + 5) % 10)
|
||||
h, m, s, ms = ms_to_times(round_ms)
|
||||
cs = ms // 10
|
||||
|
||||
return f"{h:01d}:{m:02d}:{s:02d}.{cs:02d}"
|
||||
|
||||
@classmethod
|
||||
def guess_format(cls, text):
|
||||
def guess_format(cls, text: str) -> Optional[str]:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.guess_format()`"""
|
||||
if re.search(r"V4\+ Styles", text, re.IGNORECASE):
|
||||
return "ass"
|
||||
elif re.search(r"V4 Styles", text, re.IGNORECASE):
|
||||
return "ssa"
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, subs: "pysubs2.SSAFile", fp, format_, **kwargs):
|
||||
def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.from_file()`"""
|
||||
|
||||
def string_to_field(f: str, v: str):
|
||||
def string_to_field(f: str, v: str) -> Any:
|
||||
# Per issue #45, we should handle the case where there is extra whitespace around the values.
|
||||
# Extra whitespace is removed in non-string fields where it would break the parser otherwise,
|
||||
# and in font name (where it doesn't really make sense). It is preserved in Dialogue string
|
||||
|
@ -199,9 +205,13 @@ class SubstationFormat(FormatBase):
|
|||
v = v.strip()
|
||||
return rgba_to_color(v)
|
||||
elif f in {"bold", "underline", "italic", "strikeout"}:
|
||||
return v == "-1"
|
||||
return v != "0"
|
||||
elif f in {"borderstyle", "encoding", "marginl", "marginr", "marginv", "layer", "alphalevel"}:
|
||||
return int(v)
|
||||
try:
|
||||
return int(v)
|
||||
except ValueError:
|
||||
warnings.warn(f"Failed to parse {f}, using default", RuntimeWarning)
|
||||
return 0
|
||||
elif f in {"fontsize", "scalex", "scaley", "spacing", "angle", "outline", "shadow"}:
|
||||
return float(v)
|
||||
elif f == "marked":
|
||||
|
@ -244,7 +254,8 @@ class SubstationFormat(FormatBase):
|
|||
inside_font_section = "Fonts" in line
|
||||
inside_graphic_section = "Graphics" in line
|
||||
elif inside_info_section or inside_aegisub_section:
|
||||
if line.startswith(";"): continue # skip comments
|
||||
if line.startswith(";"):
|
||||
continue # skip comments
|
||||
try:
|
||||
k, v = line.split(":", 1)
|
||||
if inside_info_section:
|
||||
|
@ -280,7 +291,7 @@ class SubstationFormat(FormatBase):
|
|||
elif line.startswith("Style:"):
|
||||
_, rest = line.split(":", 1)
|
||||
buf = rest.strip().split(",")
|
||||
name, raw_fields = buf[0], buf[1:] # splat workaround for Python 2.7
|
||||
name, *raw_fields = buf
|
||||
field_dict = {f: string_to_field(f, v) for f, v in zip(STYLE_FIELDS[format_], raw_fields)}
|
||||
sty = SSAStyle(**field_dict)
|
||||
subs.styles[name] = sty
|
||||
|
@ -307,7 +318,7 @@ class SubstationFormat(FormatBase):
|
|||
current_attachment_name = None
|
||||
|
||||
@classmethod
|
||||
def to_file(cls, subs: "pysubs2.SSAFile", fp, format_, header_notice=NOTICE, **kwargs):
|
||||
def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, header_notice: str = NOTICE, **kwargs: Any) -> None:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.to_file()`"""
|
||||
print("[Script Info]", file=fp)
|
||||
for line in header_notice.splitlines(False):
|
||||
|
@ -322,7 +333,7 @@ class SubstationFormat(FormatBase):
|
|||
for k, v in subs.aegisub_project.items():
|
||||
print(k, v, sep=": ", file=fp)
|
||||
|
||||
def field_to_string(f: str, v: Any, line: Union[SSAEvent, SSAStyle]):
|
||||
def field_to_string(f: str, v: Any, line: Union[SSAEvent, SSAStyle]) -> str:
|
||||
if f in {"start", "end"}:
|
||||
return cls.ms_to_timestamp(v)
|
||||
elif f == "marked":
|
||||
|
@ -340,8 +351,12 @@ class SubstationFormat(FormatBase):
|
|||
return str(alignment.value)
|
||||
elif isinstance(v, bool):
|
||||
return "-1" if v else "0"
|
||||
elif isinstance(v, (str, Number)):
|
||||
elif isinstance(v, int):
|
||||
return str(v)
|
||||
elif isinstance(v, float):
|
||||
return str(int(v) if v.is_integer() else v)
|
||||
elif isinstance(v, str):
|
||||
return v
|
||||
elif isinstance(v, Color):
|
||||
if format_ == "ass":
|
||||
return color_to_ass_rgba(v)
|
|
@ -1,11 +1,14 @@
|
|||
import re
|
||||
import warnings
|
||||
from typing import Optional, TextIO, Any
|
||||
|
||||
from .formatbase import FormatBase
|
||||
from .ssaevent import SSAEvent
|
||||
from .ssastyle import SSAStyle
|
||||
from .base import FormatBase
|
||||
from ..ssaevent import SSAEvent
|
||||
from ..ssastyle import SSAStyle
|
||||
from .substation import parse_tags
|
||||
from .time import ms_to_times, make_time, TIMESTAMP_SHORT, timestamp_to_ms
|
||||
from ..time import ms_to_times, make_time, TIMESTAMP_SHORT, timestamp_to_ms
|
||||
from ..ssafile import SSAFile
|
||||
|
||||
|
||||
#: Pattern that matches TMP line
|
||||
TMP_LINE = re.compile(r"(\d{1,2}:\d{2}:\d{2}):(.+)")
|
||||
|
@ -29,7 +32,7 @@ class TmpFormat(FormatBase):
|
|||
return f"{h:02d}:{m:02d}:{s:02d}"
|
||||
|
||||
@classmethod
|
||||
def guess_format(cls, text):
|
||||
def guess_format(cls, text: str) -> Optional[str]:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.guess_format()`"""
|
||||
if "[Script Info]" in text or "[V4+ Styles]" in text:
|
||||
# disambiguation vs. SSA/ASS
|
||||
|
@ -39,14 +42,16 @@ class TmpFormat(FormatBase):
|
|||
if TMP_LINE.match(line) and len(TMP_LINE.findall(line)) == 1:
|
||||
return "tmp"
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, subs, fp, format_, **kwargs):
|
||||
def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.from_file()`"""
|
||||
events = []
|
||||
|
||||
def prepare_text(text):
|
||||
def prepare_text(text: str) -> str:
|
||||
text = text.replace("|", r"\N") # convert newlines
|
||||
text = re.sub(r"< *u *>", "{\\\\u1}", text) # not r" for Python 2.7 compat, triggers unicodeescape
|
||||
text = re.sub(r"< *u *>", r"{\\u1}", text)
|
||||
text = re.sub(r"< */? *[a-zA-Z][^>]*>", "", text) # strip other HTML tags
|
||||
return text
|
||||
|
||||
|
@ -56,7 +61,9 @@ class TmpFormat(FormatBase):
|
|||
continue
|
||||
|
||||
start, text = match.groups()
|
||||
start = timestamp_to_ms(TIMESTAMP_SHORT.match(start).groups())
|
||||
match2 = TIMESTAMP_SHORT.match(start)
|
||||
assert match2 is not None, "TMP_LINE contains TIMESTAMP_SHORT"
|
||||
start = timestamp_to_ms(match2.groups())
|
||||
|
||||
# Unfortunately, end timestamp is not given; try to estimate something reasonable:
|
||||
# start + 500 ms + 67 ms/character (15 chars per second)
|
||||
|
@ -72,7 +79,7 @@ class TmpFormat(FormatBase):
|
|||
subs.events = events
|
||||
|
||||
@classmethod
|
||||
def to_file(cls, subs, fp, format_, apply_styles=True, **kwargs):
|
||||
def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, apply_styles: bool = True, **kwargs: Any) -> None:
|
||||
"""
|
||||
See :meth:`pysubs2.formats.FormatBase.to_file()`
|
||||
|
||||
|
@ -82,28 +89,24 @@ class TmpFormat(FormatBase):
|
|||
apply_styles: If False, do not write any styling.
|
||||
|
||||
"""
|
||||
def prepare_text(text, style):
|
||||
def prepare_text(text: str, style: SSAStyle) -> str:
|
||||
body = []
|
||||
skip = False
|
||||
for fragment, sty in parse_tags(text, style, subs.styles):
|
||||
fragment = fragment.replace(r"\h", " ")
|
||||
fragment = fragment.replace(r"\n", "\n")
|
||||
fragment = fragment.replace(r"\N", "\n")
|
||||
if apply_styles:
|
||||
if sty.italic: fragment = f"<i>{fragment}</i>"
|
||||
if sty.underline: fragment = f"<u>{fragment}</u>"
|
||||
if sty.strikeout: fragment = f"<s>{fragment}</s>"
|
||||
if sty.drawing: skip = True
|
||||
if sty.italic:
|
||||
fragment = f"<i>{fragment}</i>"
|
||||
if sty.underline:
|
||||
fragment = f"<u>{fragment}</u>"
|
||||
if sty.strikeout:
|
||||
fragment = f"<s>{fragment}</s>"
|
||||
body.append(fragment)
|
||||
|
||||
if skip:
|
||||
return ""
|
||||
else:
|
||||
return re.sub("\n+", "\n", "".join(body).strip())
|
||||
return re.sub("\n+", "\n", "".join(body).strip())
|
||||
|
||||
visible_lines = (line for line in subs if not line.is_comment)
|
||||
|
||||
for line in visible_lines:
|
||||
for line in subs.get_text_events():
|
||||
start = cls.ms_to_timestamp(line.start)
|
||||
text = prepare_text(line.text, subs.styles.get(line.style, SSAStyle.DEFAULT_STYLE))
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import re
|
||||
from typing import List
|
||||
from typing import List, Sequence, Optional, TextIO, Any
|
||||
|
||||
import pysubs2
|
||||
from ..ssaevent import SSAEvent
|
||||
from .subrip import SubripFormat
|
||||
from .time import make_time
|
||||
from ..time import make_time
|
||||
from ..ssafile import SSAFile
|
||||
|
||||
|
||||
class WebVTTFormat(SubripFormat):
|
||||
"""
|
||||
Web Video Text Tracks (WebVTT) subtitle format implementation
|
||||
|
||||
Currently, this shares implementation with :class:`pysubs2.subrip.SubripFormat`.
|
||||
Currently, this shares implementation with :class:`pysubs2.formats.subrip.SubripFormat`.
|
||||
"""
|
||||
TIMESTAMP = re.compile(r"(\d{0,4}:)?(\d{2}):(\d{2})\.(\d{2,3})")
|
||||
|
||||
|
@ -20,7 +21,7 @@ class WebVTTFormat(SubripFormat):
|
|||
return result.replace(',', '.')
|
||||
|
||||
@staticmethod
|
||||
def timestamp_to_ms(groups):
|
||||
def timestamp_to_ms(groups: Sequence[str]) -> int:
|
||||
_h, _m, _s, _ms = groups
|
||||
if not _h:
|
||||
h = 0
|
||||
|
@ -30,22 +31,24 @@ class WebVTTFormat(SubripFormat):
|
|||
return make_time(h=h, m=m, s=s, ms=ms)
|
||||
|
||||
@classmethod
|
||||
def guess_format(cls, text):
|
||||
def guess_format(cls, text: str) -> Optional[str]:
|
||||
"""See :meth:`pysubs2.formats.FormatBase.guess_format()`"""
|
||||
if text.lstrip().startswith("WEBVTT"):
|
||||
return "vtt"
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def to_file(cls, subs, fp, format_, **kwargs):
|
||||
def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: # type: ignore[override]
|
||||
"""
|
||||
See :meth:`pysubs2.formats.FormatBase.to_file()`
|
||||
See :meth:`pysubs2.formats.SubripFormat.to_file()`, additional SRT options are supported by VTT as well
|
||||
"""
|
||||
print("WEBVTT\n", file=fp)
|
||||
return super(WebVTTFormat, cls).to_file(
|
||||
subs=subs, fp=fp, format_=format_, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _get_visible_lines(cls, subs: "pysubs2.SSAFile") -> List["pysubs2.SSAEvent"]:
|
||||
visible_lines = [line for line in subs if not line.is_comment]
|
||||
def _get_visible_lines(cls, subs: "SSAFile") -> List[SSAEvent]:
|
||||
visible_lines = super()._get_visible_lines(subs)
|
||||
visible_lines.sort(key=lambda e: e.start)
|
||||
return visible_lines
|
|
@ -5,9 +5,9 @@ See https://github.com/openai/whisper
|
|||
|
||||
"""
|
||||
|
||||
from .ssaevent import SSAEvent
|
||||
from .ssafile import SSAFile
|
||||
from .time import make_time
|
||||
from ..ssaevent import SSAEvent
|
||||
from ..ssafile import SSAFile
|
||||
from ..time import make_time
|
||||
from typing import Union, List, Dict, Any
|
||||
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import re
|
||||
|
||||
from .time import times_to_ms
|
||||
from .formatbase import FormatBase
|
||||
from .ssaevent import SSAEvent
|
||||
|
||||
|
||||
# thanks to http://otsaloma.io/gaupol/doc/api/aeidon.files.mpl2_source.html
|
||||
MPL2_FORMAT = re.compile(r"^\[(-?\d+)\]\[(-?\d+)\](.*)", re.MULTILINE)
|
||||
|
||||
|
||||
class MPL2Format(FormatBase):
|
||||
"""MPL2 subtitle format implementation"""
|
||||
@classmethod
|
||||
def guess_format(cls, text):
|
||||
"""See :meth:`pysubs2.formats.FormatBase.guess_format()`"""
|
||||
if MPL2_FORMAT.search(text):
|
||||
return "mpl2"
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, subs, fp, format_, **kwargs):
|
||||
"""See :meth:`pysubs2.formats.FormatBase.from_file()`"""
|
||||
def prepare_text(lines):
|
||||
out = []
|
||||
for s in lines.split("|"):
|
||||
s = s.strip()
|
||||
|
||||
if s.startswith("/"):
|
||||
# line beginning with '/' is in italics
|
||||
s = r"{\i1}%s{\i0}" % s[1:].strip()
|
||||
|
||||
out.append(s)
|
||||
return "\\N".join(out)
|
||||
|
||||
subs.events = [SSAEvent(start=times_to_ms(s=float(start) / 10), end=times_to_ms(s=float(end) / 10),
|
||||
text=prepare_text(text)) for start, end, text in MPL2_FORMAT.findall(fp.getvalue())]
|
||||
|
||||
@classmethod
|
||||
def to_file(cls, subs, fp, format_, **kwargs):
|
||||
"""
|
||||
See :meth:`pysubs2.formats.FormatBase.to_file()`
|
||||
|
||||
No styling is supported at the moment.
|
||||
|
||||
"""
|
||||
# TODO handle italics
|
||||
for line in subs:
|
||||
if line.is_comment:
|
||||
continue
|
||||
|
||||
print("[{start}][{end}] {text}".format(start=int(line.start // 100),
|
||||
end=int(line.end // 100),
|
||||
text=line.plaintext.replace("\n", "|")),
|
||||
file=fp)
|
|
@ -1,6 +1,6 @@
|
|||
import re
|
||||
import warnings
|
||||
from typing import Optional, Dict, Any, ClassVar
|
||||
from typing import Optional, Dict, Any, ClassVar, FrozenSet
|
||||
import dataclasses
|
||||
|
||||
from .common import IntOrFloat
|
||||
|
@ -41,7 +41,7 @@ class SSAEvent:
|
|||
type: str = "Dialogue" #: Line type (Dialogue/Comment)
|
||||
|
||||
@property
|
||||
def FIELDS(self):
|
||||
def FIELDS(self) -> FrozenSet[str]:
|
||||
"""All fields in SSAEvent."""
|
||||
warnings.warn("Deprecated in 1.2.0 - it's a dataclass now", DeprecationWarning)
|
||||
return frozenset(field.name for field in dataclasses.fields(self))
|
||||
|
@ -57,7 +57,7 @@ class SSAEvent:
|
|||
return self.end - self.start
|
||||
|
||||
@duration.setter
|
||||
def duration(self, ms: int):
|
||||
def duration(self, ms: int) -> None:
|
||||
if ms >= 0:
|
||||
self.end = self.start + ms
|
||||
else:
|
||||
|
@ -74,7 +74,7 @@ class SSAEvent:
|
|||
return self.type == "Comment"
|
||||
|
||||
@is_comment.setter
|
||||
def is_comment(self, value: bool):
|
||||
def is_comment(self, value: bool) -> None:
|
||||
if value:
|
||||
self.type = "Comment"
|
||||
else:
|
||||
|
@ -83,9 +83,18 @@ class SSAEvent:
|
|||
@property
|
||||
def is_drawing(self) -> bool:
|
||||
"""Returns True if line is SSA drawing tag (ie. not text)"""
|
||||
from .substation import parse_tags
|
||||
from .formats.substation import parse_tags
|
||||
return any(sty.drawing for _, sty in parse_tags(self.text))
|
||||
|
||||
@property
|
||||
def is_text(self) -> bool:
|
||||
"""
|
||||
Returns False for SSA drawings and comment lines, True otherwise
|
||||
|
||||
In general, for non-SSA formats these events should be ignored.
|
||||
"""
|
||||
return not (self.is_comment or self.is_drawing)
|
||||
|
||||
@property
|
||||
def plaintext(self) -> str:
|
||||
"""
|
||||
|
@ -102,11 +111,11 @@ class SSAEvent:
|
|||
return text
|
||||
|
||||
@plaintext.setter
|
||||
def plaintext(self, text: str):
|
||||
def plaintext(self, text: str) -> None:
|
||||
self.text = text.replace("\n", r"\N")
|
||||
|
||||
def shift(self, h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloat=0,
|
||||
frames: Optional[int]=None, fps: Optional[float]=None):
|
||||
def shift(self, h: IntOrFloat = 0, m: IntOrFloat = 0, s: IntOrFloat = 0, ms: IntOrFloat = 0,
|
||||
frames: Optional[int] = None, fps: Optional[float] = None) -> None:
|
||||
"""
|
||||
Shift start and end times.
|
||||
|
||||
|
@ -132,36 +141,36 @@ class SSAEvent:
|
|||
else:
|
||||
raise TypeError("Cannot compare to non-SSAEvent object")
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
def __eq__(self, other: object) -> bool:
|
||||
# XXX document this
|
||||
if not isinstance(other, SSAEvent):
|
||||
return NotImplemented
|
||||
return self.start == other.start and self.end == other.end
|
||||
|
||||
def __ne__(self, other) -> bool:
|
||||
def __ne__(self, other: object) -> bool:
|
||||
if not isinstance(other, SSAEvent):
|
||||
return NotImplemented
|
||||
return self.start != other.start or self.end != other.end
|
||||
|
||||
def __lt__(self, other) -> bool:
|
||||
def __lt__(self, other: object) -> bool:
|
||||
if not isinstance(other, SSAEvent):
|
||||
return NotImplemented
|
||||
return (self.start, self.end) < (other.start, other.end)
|
||||
|
||||
def __le__(self, other) -> bool:
|
||||
def __le__(self, other: object) -> bool:
|
||||
if not isinstance(other, SSAEvent):
|
||||
return NotImplemented
|
||||
return (self.start, self.end) <= (other.start, other.end)
|
||||
|
||||
def __gt__(self, other) -> bool:
|
||||
def __gt__(self, other: object) -> bool:
|
||||
if not isinstance(other, SSAEvent):
|
||||
return NotImplemented
|
||||
return (self.start, self.end) > (other.start, other.end)
|
||||
|
||||
def __ge__(self, other) -> bool:
|
||||
def __ge__(self, other: object) -> bool:
|
||||
if not isinstance(other, SSAEvent):
|
||||
return NotImplemented
|
||||
return (self.start, self.end) >= (other.start, other.end)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<SSAEvent type={self.type} start={ms_to_str(self.start)} end={ms_to_str(self.end)} text={self.text!r}>"
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
from collections.abc import MutableSequence
|
||||
import io
|
||||
from io import open
|
||||
from itertools import chain
|
||||
import os.path
|
||||
import logging
|
||||
from typing import Optional, List, Dict, Iterable, Any, overload, Iterator
|
||||
from typing import Optional, List, Dict, Iterable, Any, overload, Iterator, TextIO, Tuple, MutableSequence
|
||||
|
||||
from .common import IntOrFloat
|
||||
from .formats import autodetect_format, get_format_class, get_format_identifier
|
||||
from .substation import is_valid_field_content
|
||||
from .ssaevent import SSAEvent
|
||||
from .ssastyle import SSAStyle
|
||||
from .time import make_time, ms_to_str
|
||||
|
||||
|
||||
class SSAFile(MutableSequence):
|
||||
class SSAFile(MutableSequence[SSAEvent]):
|
||||
"""
|
||||
Subtitle file in SubStation Alpha format.
|
||||
|
||||
|
@ -32,7 +28,7 @@ class SSAFile(MutableSequence):
|
|||
|
||||
"""
|
||||
|
||||
DEFAULT_INFO = {
|
||||
DEFAULT_INFO: Dict[str, str] = {
|
||||
"WrapStyle": "0",
|
||||
"ScaledBorderAndShadow": "yes",
|
||||
"Collisions": "Normal"
|
||||
|
@ -53,7 +49,8 @@ class SSAFile(MutableSequence):
|
|||
# ------------------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str, encoding: str="utf-8", format_: Optional[str]=None, fps: Optional[float]=None, **kwargs) -> "SSAFile":
|
||||
def load(cls, path: str, encoding: str = "utf-8", format_: Optional[str] = None, fps: Optional[float] = None,
|
||||
errors: Optional[str] = None, **kwargs: Any) -> "SSAFile":
|
||||
"""
|
||||
Load subtitle file from given path.
|
||||
|
||||
|
@ -62,12 +59,23 @@ class SSAFile(MutableSequence):
|
|||
See also:
|
||||
Specific formats may implement additional loading options,
|
||||
please refer to documentation of the implementation classes
|
||||
(eg. :meth:`pysubs2.subrip.SubripFormat.from_file()`)
|
||||
(eg. :meth:`pysubs2.formats.subrip.SubripFormat.from_file()`)
|
||||
|
||||
Arguments:
|
||||
path (str): Path to subtitle file.
|
||||
encoding (str): Character encoding of input file.
|
||||
Defaults to UTF-8, you may need to change this.
|
||||
errors (Optional[str]): Error handling for character encoding
|
||||
of input file. Defaults to ``None``; use the value ``"surrogateescape"``
|
||||
for pass-through of bytes not supported by selected encoding via
|
||||
`Unicode surrogate pairs <https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates>`_.
|
||||
See documentation of builtin ``open()`` function for more.
|
||||
|
||||
.. versionchanged:: 1.7.0
|
||||
The ``errors`` parameter was introduced to facilitate
|
||||
pass-through of subtitle files with unknown text encoding.
|
||||
Previous versions of the library behaved as if ``errors=None``.
|
||||
|
||||
format_ (str): Optional, forces use of specific parser
|
||||
(eg. `"srt"`, `"ass"`). Otherwise, format is detected
|
||||
automatically from file contents. This argument should
|
||||
|
@ -96,23 +104,32 @@ class SSAFile(MutableSequence):
|
|||
|
||||
Example:
|
||||
>>> subs1 = pysubs2.load("subrip-subtitles.srt")
|
||||
>>> subs2 = pysubs2.load("microdvd-subtitles.sub", fps=23.976)
|
||||
>>> subs3 = pysubs2.load("subrip-subtitles-with-fancy-tags.srt", keep_unknown_html_tags=True)
|
||||
>>> subs2 = pysubs2.load("microdvd-subtitles.sub",fps=23.976)
|
||||
>>> subs3 = pysubs2.load("subrip-subtitles-with-fancy-tags.srt",keep_unknown_html_tags=True)
|
||||
|
||||
"""
|
||||
with open(path, encoding=encoding) as fp:
|
||||
with open(path, encoding=encoding, errors=errors) as fp:
|
||||
return cls.from_file(fp, format_, fps=fps, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, string: str, format_: Optional[str]=None, fps: Optional[float]=None, **kwargs) -> "SSAFile":
|
||||
def from_string(cls, string: str, format_: Optional[str] = None, fps: Optional[float] = None,
|
||||
**kwargs: Any) -> "SSAFile":
|
||||
"""
|
||||
Load subtitle file from string.
|
||||
|
||||
See :meth:`SSAFile.load()` for full description.
|
||||
|
||||
Arguments:
|
||||
string (str): Subtitle file in a string. Note that the string
|
||||
must be Unicode (in Python 2).
|
||||
string (str): Subtitle file in a string. Note that the string must be Unicode (``str``, not ``bytes``).
|
||||
format_ (str): Optional, forces use of specific parser
|
||||
(eg. `"srt"`, `"ass"`). Otherwise, format is detected
|
||||
automatically from file contents. This argument should
|
||||
be rarely needed.
|
||||
fps (float): Framerate for frame-based formats (MicroDVD),
|
||||
for other formats this argument is ignored. Framerate might
|
||||
be detected from the file, in which case you don't need
|
||||
to specify it here (when given, this argument overrides
|
||||
autodetection).
|
||||
|
||||
Returns:
|
||||
SSAFile
|
||||
|
@ -130,7 +147,8 @@ class SSAFile(MutableSequence):
|
|||
return cls.from_file(fp, format_, fps=fps, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, fp: io.TextIOBase, format_: Optional[str]=None, fps: Optional[float]=None, **kwargs) -> "SSAFile":
|
||||
def from_file(cls, fp: TextIO, format_: Optional[str] = None, fps: Optional[float] = None,
|
||||
**kwargs: Any) -> "SSAFile":
|
||||
"""
|
||||
Read subtitle file from file object.
|
||||
|
||||
|
@ -141,8 +159,17 @@ class SSAFile(MutableSequence):
|
|||
or :meth:`SSAFile.from_string()` is preferable.
|
||||
|
||||
Arguments:
|
||||
fp (file object): A file object, ie. :class:`io.TextIOBase` instance.
|
||||
fp (file object): A file object, ie. :class:`TextIO` instance.
|
||||
Note that the file must be opened in text mode (as opposed to binary).
|
||||
format_ (str): Optional, forces use of specific parser
|
||||
(eg. `"srt"`, `"ass"`). Otherwise, format is detected
|
||||
automatically from file contents. This argument should
|
||||
be rarely needed.
|
||||
fps (float): Framerate for frame-based formats (MicroDVD),
|
||||
for other formats this argument is ignored. Framerate might
|
||||
be detected from the file, in which case you don't need
|
||||
to specify it here (when given, this argument overrides
|
||||
autodetection).
|
||||
|
||||
Returns:
|
||||
SSAFile
|
||||
|
@ -164,7 +191,8 @@ class SSAFile(MutableSequence):
|
|||
impl.from_file(subs, fp, format_, fps=fps, **kwargs)
|
||||
return subs
|
||||
|
||||
def save(self, path: str, encoding: str="utf-8", format_: Optional[str]=None, fps: Optional[float]=None, **kwargs):
|
||||
def save(self, path: str, encoding: str = "utf-8", format_: Optional[str] = None, fps: Optional[float] = None,
|
||||
errors: Optional[str] = None, **kwargs: Any) -> None:
|
||||
"""
|
||||
Save subtitle file to given path.
|
||||
|
||||
|
@ -173,7 +201,7 @@ class SSAFile(MutableSequence):
|
|||
See also:
|
||||
Specific formats may implement additional saving options,
|
||||
please refer to documentation of the implementation classes
|
||||
(eg. :meth:`pysubs2.subrip.SubripFormat.to_file()`)
|
||||
(eg. :meth:`pysubs2.formats.subrip.SubripFormat.to_file()`)
|
||||
|
||||
Arguments:
|
||||
path (str): Path to subtitle file.
|
||||
|
@ -191,6 +219,17 @@ class SSAFile(MutableSequence):
|
|||
different framerate, use this argument. See also
|
||||
:meth:`SSAFile.transform_framerate()` for fixing bad
|
||||
frame-based to time-based conversions.
|
||||
errors (Optional[str]): Error handling for character encoding
|
||||
of input file. Defaults to ``None``; use the value ``"surrogateescape"``
|
||||
for pass-through of bytes not supported by selected encoding via
|
||||
`Unicode surrogate pairs <https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates>`_.
|
||||
See documentation of builtin ``open()`` function for more.
|
||||
|
||||
.. versionchanged:: 1.7.0
|
||||
The ``errors`` parameter was introduced to facilitate
|
||||
pass-through of subtitle files with unknown text encoding.
|
||||
Previous versions of the library behaved as if ``errors=None``.
|
||||
|
||||
kwargs: Extra options for the writer.
|
||||
|
||||
Raises:
|
||||
|
@ -205,10 +244,10 @@ class SSAFile(MutableSequence):
|
|||
ext = os.path.splitext(path)[1].lower()
|
||||
format_ = get_format_identifier(ext)
|
||||
|
||||
with open(path, "w", encoding=encoding) as fp:
|
||||
with open(path, "w", encoding=encoding, errors=errors) as fp:
|
||||
self.to_file(fp, format_, fps=fps, **kwargs)
|
||||
|
||||
def to_string(self, format_: str, fps: Optional[float]=None, **kwargs) -> str:
|
||||
def to_string(self, format_: str, fps: Optional[float] = None, **kwargs: Any) -> str:
|
||||
"""
|
||||
Get subtitle file as a string.
|
||||
|
||||
|
@ -222,7 +261,7 @@ class SSAFile(MutableSequence):
|
|||
self.to_file(fp, format_, fps=fps, **kwargs)
|
||||
return fp.getvalue()
|
||||
|
||||
def to_file(self, fp: io.TextIOBase, format_: str, fps: Optional[float]=None, **kwargs):
|
||||
def to_file(self, fp: TextIO, format_: str, fps: Optional[float] = None, **kwargs: Any) -> None:
|
||||
"""
|
||||
Write subtitle file to file object.
|
||||
|
||||
|
@ -233,7 +272,7 @@ class SSAFile(MutableSequence):
|
|||
or :meth:`SSAFile.to_string()` is preferable.
|
||||
|
||||
Arguments:
|
||||
fp (file object): A file object, ie. :class:`io.TextIOBase` instance.
|
||||
fp (file object): A file object, ie. :class:`TextIO` instance.
|
||||
Note that the file must be opened in text mode (as opposed to binary).
|
||||
|
||||
"""
|
||||
|
@ -244,8 +283,8 @@ class SSAFile(MutableSequence):
|
|||
# Retiming subtitles
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def shift(self, h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloat=0,
|
||||
frames: Optional[int]=None, fps: Optional[float]=None):
|
||||
def shift(self, h: IntOrFloat = 0, m: IntOrFloat = 0, s: IntOrFloat = 0, ms: IntOrFloat = 0,
|
||||
frames: Optional[int] = None, fps: Optional[float] = None) -> None:
|
||||
"""
|
||||
Shift all subtitles by constant time amount.
|
||||
|
||||
|
@ -253,7 +292,10 @@ class SSAFile(MutableSequence):
|
|||
case, specify both frames and fps. h, m, s, ms will be ignored.
|
||||
|
||||
Arguments:
|
||||
h, m, s, ms: Integer or float values, may be positive or negative.
|
||||
h: Integer or float values, may be positive or negative (hours).
|
||||
m: Integer or float values, may be positive or negative (minutes).
|
||||
s: Integer or float values, may be positive or negative (seconds).
|
||||
ms: Integer or float values, may be positive or negative (milliseconds).
|
||||
frames (int): When specified, must be an integer number of frames.
|
||||
May be positive or negative. fps must be also specified.
|
||||
fps (float): When specified, must be a positive number.
|
||||
|
@ -267,7 +309,7 @@ class SSAFile(MutableSequence):
|
|||
line.start += delta
|
||||
line.end += delta
|
||||
|
||||
def transform_framerate(self, in_fps: float, out_fps: float):
|
||||
def transform_framerate(self, in_fps: float, out_fps: float) -> None:
|
||||
"""
|
||||
Rescale all timestamps by ratio of in_fps/out_fps.
|
||||
|
||||
|
@ -294,7 +336,7 @@ class SSAFile(MutableSequence):
|
|||
# Working with styles
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def rename_style(self, old_name: str, new_name: str):
|
||||
def rename_style(self, old_name: str, new_name: str) -> None:
|
||||
"""
|
||||
Rename a style, including references to it.
|
||||
|
||||
|
@ -308,6 +350,8 @@ class SSAFile(MutableSequence):
|
|||
or new_name is taken.
|
||||
|
||||
"""
|
||||
from .formats.substation import is_valid_field_content
|
||||
|
||||
if old_name not in self.styles:
|
||||
raise KeyError(f"Style {old_name!r} not found")
|
||||
if new_name in self.styles:
|
||||
|
@ -323,7 +367,7 @@ class SSAFile(MutableSequence):
|
|||
if line.style == old_name:
|
||||
line.style = new_name
|
||||
|
||||
def import_styles(self, subs: "SSAFile", overwrite: bool=True):
|
||||
def import_styles(self, subs: "SSAFile", overwrite: bool = True) -> None:
|
||||
"""
|
||||
Merge in styles from other SSAFile.
|
||||
|
||||
|
@ -344,7 +388,7 @@ class SSAFile(MutableSequence):
|
|||
# Helper methods
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def remove_miscellaneous_events(self):
|
||||
def remove_miscellaneous_events(self) -> None:
|
||||
"""
|
||||
Remove subtitles which appear to be non-essential (the --clean in CLI)
|
||||
|
||||
|
@ -357,7 +401,7 @@ class SSAFile(MutableSequence):
|
|||
new_events = []
|
||||
|
||||
duplicate_text_ids = set()
|
||||
times_to_texts = {}
|
||||
times_to_texts: Dict[Tuple[int, int], List[str]] = {}
|
||||
for i, e in enumerate(self):
|
||||
tmp = times_to_texts.setdefault((e.start, e.end), [])
|
||||
if tmp.count(e.plaintext) > 0:
|
||||
|
@ -376,7 +420,13 @@ class SSAFile(MutableSequence):
|
|||
|
||||
self.events = new_events
|
||||
|
||||
def equals(self, other: "SSAFile"):
|
||||
def get_text_events(self) -> List[SSAEvent]:
|
||||
"""
|
||||
Return list of events excluding SSA comment lines and lines with SSA drawing tags
|
||||
"""
|
||||
return [e for e in self if e.is_text]
|
||||
|
||||
def equals(self, other: "SSAFile") -> bool:
|
||||
"""
|
||||
Equality of two SSAFiles.
|
||||
|
||||
|
@ -435,7 +485,8 @@ class SSAFile(MutableSequence):
|
|||
return False
|
||||
elif self_style != other_style:
|
||||
for k in self_style.FIELDS:
|
||||
if getattr(self_style, k) != getattr(other_style, k): logging.debug("difference in field %r", k)
|
||||
if getattr(self_style, k) != getattr(other_style, k):
|
||||
logging.debug("difference in field %r", k)
|
||||
logging.debug("style %r differs (self=%r, other=%r)", key, self_style.as_dict(), other_style.as_dict())
|
||||
return False
|
||||
|
||||
|
@ -446,7 +497,8 @@ class SSAFile(MutableSequence):
|
|||
for i, (self_event, other_event) in enumerate(zip(self.events, other.events)):
|
||||
if not self_event.equals(other_event):
|
||||
for k in self_event.FIELDS:
|
||||
if getattr(self_event, k) != getattr(other_event, k): logging.debug("difference in field %r", k)
|
||||
if getattr(self_event, k) != getattr(other_event, k):
|
||||
logging.debug("difference in field %r", k)
|
||||
logging.debug("event %d differs (self=%r, other=%r)", i, self_event.as_dict(), other_event.as_dict())
|
||||
return False
|
||||
|
||||
|
@ -454,7 +506,7 @@ class SSAFile(MutableSequence):
|
|||
else:
|
||||
raise TypeError("Cannot compare to non-SSAFile object")
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
if self.events:
|
||||
max_time = max(ev.end for ev in self)
|
||||
s = f"<SSAFile with {len(self)} events and {len(self.styles)} styles, last timestamp {ms_to_str(max_time)}>"
|
||||
|
@ -467,7 +519,7 @@ class SSAFile(MutableSequence):
|
|||
# MutableSequence implementation + sort()
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def sort(self):
|
||||
def sort(self) -> None:
|
||||
"""Sort subtitles time-wise, in-place."""
|
||||
self.events.sort()
|
||||
|
||||
|
@ -476,24 +528,24 @@ class SSAFile(MutableSequence):
|
|||
|
||||
@overload
|
||||
def __getitem__(self, item: int) -> SSAEvent:
|
||||
return self.events[item]
|
||||
pass
|
||||
|
||||
@overload
|
||||
def __getitem__(self, s: slice) -> List[SSAEvent]:
|
||||
return self.events[s]
|
||||
pass
|
||||
|
||||
def __getitem__(self, item):
|
||||
def __getitem__(self, item: Any) -> Any:
|
||||
return self.events[item]
|
||||
|
||||
@overload
|
||||
def __setitem__(self, key: int, value: SSAEvent):
|
||||
def __setitem__(self, key: int, value: SSAEvent) -> None:
|
||||
pass
|
||||
|
||||
@overload
|
||||
def __setitem__(self, keys: slice, values: Iterable[SSAEvent]):
|
||||
def __setitem__(self, keys: slice, values: Iterable[SSAEvent]) -> None:
|
||||
pass
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
def __setitem__(self, key: Any, value: Any) -> None:
|
||||
if isinstance(key, int):
|
||||
if isinstance(value, SSAEvent):
|
||||
self.events[key] = value
|
||||
|
@ -509,21 +561,24 @@ class SSAFile(MutableSequence):
|
|||
raise TypeError("Bad key type")
|
||||
|
||||
@overload
|
||||
def __delitem__(self, key: int):
|
||||
def __delitem__(self, key: int) -> None:
|
||||
pass
|
||||
|
||||
@overload
|
||||
def __delitem__(self, s: slice):
|
||||
def __delitem__(self, s: slice) -> None:
|
||||
pass
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: Any) -> None:
|
||||
del self.events[key]
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self.events)
|
||||
|
||||
def insert(self, index: int, value: SSAEvent):
|
||||
def insert(self, index: int, value: SSAEvent) -> None:
|
||||
if isinstance(value, SSAEvent):
|
||||
self.events.insert(index, value)
|
||||
else:
|
||||
raise TypeError("SSAFile.events must contain only SSAEvent objects")
|
||||
|
||||
|
||||
from .formats import autodetect_format, get_format_class, get_format_identifier # noqa: E402
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import warnings
|
||||
from typing import Dict, Any, ClassVar
|
||||
from typing import Dict, Any, ClassVar, FrozenSet
|
||||
import dataclasses
|
||||
|
||||
from .common import Color, Alignment
|
||||
|
@ -23,7 +23,7 @@ class SSAStyle:
|
|||
DEFAULT_STYLE: ClassVar["SSAStyle"] = None # type: ignore[assignment]
|
||||
|
||||
@property
|
||||
def FIELDS(self):
|
||||
def FIELDS(self) -> FrozenSet[str]:
|
||||
"""All fields in SSAStyle."""
|
||||
warnings.warn("Deprecated in 1.2.0 - it's a dataclass now", DeprecationWarning)
|
||||
return frozenset(field.name for field in dataclasses.fields(self))
|
||||
|
@ -66,7 +66,7 @@ class SSAStyle:
|
|||
# dataclasses.asdict() would recursively dictify Color objects, which we don't want
|
||||
return {field.name: getattr(self, field.name) for field in dataclasses.fields(self)}
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<SSAStyle {self.fontsize!r}px" \
|
||||
f"{' bold' if self.bold else ''}" \
|
||||
f"{' italic' if self.italic else ''}" \
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from collections import namedtuple
|
||||
import re
|
||||
from typing import Optional, List, Tuple, Sequence
|
||||
from pysubs2.common import IntOrFloat
|
||||
from typing import Optional, Sequence, NamedTuple
|
||||
|
||||
from .common import IntOrFloat
|
||||
|
||||
#: Pattern that matches both SubStation and SubRip timestamps.
|
||||
TIMESTAMP = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})[.,](\d{1,3})")
|
||||
|
@ -9,11 +9,17 @@ TIMESTAMP = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})[.,](\d{1,3})")
|
|||
#: Pattern that matches H:MM:SS or HH:MM:SS timestamps.
|
||||
TIMESTAMP_SHORT = re.compile(r"(\d{1,2}):(\d{2}):(\d{2})")
|
||||
|
||||
Times = namedtuple("Times", ["h", "m", "s", "ms"])
|
||||
|
||||
class Times(NamedTuple):
|
||||
"""Named tuple (h, m, s, ms) of ints."""
|
||||
h: int
|
||||
m: int
|
||||
s: int
|
||||
ms: int
|
||||
|
||||
|
||||
def make_time(h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloat=0,
|
||||
frames: Optional[int]=None, fps: Optional[float]=None):
|
||||
def make_time(h: IntOrFloat = 0, m: IntOrFloat = 0, s: IntOrFloat = 0, ms: IntOrFloat = 0,
|
||||
frames: Optional[int] = None, fps: Optional[float] = None) -> int:
|
||||
"""
|
||||
Convert time to milliseconds.
|
||||
|
||||
|
@ -38,7 +44,7 @@ def make_time(h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloat=
|
|||
raise ValueError("Both fps and frames must be specified")
|
||||
|
||||
|
||||
def timestamp_to_ms(groups: Sequence[str]):
|
||||
def timestamp_to_ms(groups: Sequence[str]) -> int:
|
||||
"""
|
||||
Convert groups from :data:`pysubs2.time.TIMESTAMP` or :data:`pysubs2.time.TIMESTAMP_SHORT`
|
||||
match to milliseconds.
|
||||
|
@ -50,6 +56,11 @@ def timestamp_to_ms(groups: Sequence[str]):
|
|||
1000
|
||||
|
||||
"""
|
||||
h: int
|
||||
m: int
|
||||
s: int
|
||||
ms: int
|
||||
frac: int
|
||||
if len(groups) == 4:
|
||||
h, m, s, frac = map(int, groups)
|
||||
ms = frac * 10**(3 - len(groups[-1]))
|
||||
|
@ -65,7 +76,7 @@ def timestamp_to_ms(groups: Sequence[str]):
|
|||
return ms
|
||||
|
||||
|
||||
def times_to_ms(h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloat=0) -> int:
|
||||
def times_to_ms(h: IntOrFloat = 0, m: IntOrFloat = 0, s: IntOrFloat = 0, ms: IntOrFloat = 0) -> int:
|
||||
"""
|
||||
Convert hours, minutes, seconds to milliseconds.
|
||||
|
||||
|
@ -124,7 +135,7 @@ def ms_to_frames(ms: IntOrFloat, fps: float) -> int:
|
|||
return int(round((ms / 1000) * fps))
|
||||
|
||||
|
||||
def ms_to_times(ms: IntOrFloat) -> Tuple[int, int, int, int]:
|
||||
def ms_to_times(ms: IntOrFloat) -> Times:
|
||||
"""
|
||||
Convert milliseconds to normalized tuple (h, m, s, ms).
|
||||
|
||||
|
@ -144,7 +155,7 @@ def ms_to_times(ms: IntOrFloat) -> Tuple[int, int, int, int]:
|
|||
return Times(h, m, s, ms)
|
||||
|
||||
|
||||
def ms_to_str(ms: IntOrFloat, fractions: bool=False) -> str:
|
||||
def ms_to_str(ms: IntOrFloat, fractions: bool = False) -> str:
|
||||
"""
|
||||
Prettyprint milliseconds to [-]H:MM:SS[.mmm]
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ knowit==0.5.2
|
|||
Mako==1.3.2
|
||||
pycountry==23.12.11
|
||||
pyrsistent==0.20.0
|
||||
pysubs2==1.6.1
|
||||
pysubs2==1.7.2
|
||||
python-engineio==4.9.0
|
||||
python-socketio==5.11.1
|
||||
pytz==2024.1
|
||||
|
|
Loading…
Reference in a new issue