-# coding: utf-8
-from __future__ import unicode_literals, print_function, division
-
"""
A partial parser for WebVTT segments. Interprets enough of the WebVTT stream
to be able to assemble a single stand-alone subtitle file, suitably adjusting
in RFC 8216 ยง3.5 <https://tools.ietf.org/html/rfc8216#section-3.5>.
"""
-import re
import io
-from .utils import int_or_none
-from .compat import (
- compat_str as str,
- compat_Pattern,
- compat_Match,
-)
+import re
+from .utils import int_or_none, timetuple_from_msec
-class _MatchParser(object):
+
+class _MatchParser:
"""
An object that maintains the current parsing position and allows
conveniently advancing it as syntax elements are successfully parsed.
self._pos = 0
def match(self, r):
- if isinstance(r, compat_Pattern):
+ if isinstance(r, re.Pattern):
return r.match(self._data, self._pos)
if isinstance(r, str):
if self._data.startswith(r, self._pos):
def advance(self, by):
if by is None:
amt = 0
- elif isinstance(by, compat_Match):
+ elif isinstance(by, re.Match):
amt = len(by.group(0))
elif isinstance(by, str):
amt = len(by)
"""
def __init__(self, parent):
- super(_MatchChildParser, self).__init__(parent._data)
+ super().__init__(parent._data)
self.__parent = parent
self._pos = parent._pos
class ParseError(Exception):
def __init__(self, parser):
- super(ParseError, self).__init__("Parse error at position %u (near %r)" % (
+ super().__init__("Parse error at position %u (near %r)" % (
parser._pos, parser._data[parser._pos:parser._pos + 20]
))
+# While the specification <https://www.w3.org/TR/webvtt1/#webvtt-timestamp>
+# prescribes that hours must be *2 or more* digits, timestamps with a single
+# digit for the hour part has been seen in the wild.
+# See https://github.com/yt-dlp/yt-dlp/issues/921
_REGEX_TS = re.compile(r'''(?x)
- (?:([0-9]{2,}):)?
+ (?:([0-9]{1,}):)?
([0-9]{2}):
([0-9]{2})\.
([0-9]{3})?
Convert a parsed WebVTT timestamp (a re.Match obtained from _REGEX_TS)
into an MPEG PES timestamp: a tick counter at 90 kHz resolution.
"""
-
- h, min, s, ms = ts.groups()
- return 90 * (
- int(h or 0) * 3600000 + # noqa: W504,E221,E222
- int(min) * 60000 + # noqa: W504,E221,E222
- int(s) * 1000 + # noqa: W504,E221,E222
- int(ms) # noqa: W504,E221,E222
- )
+ return 90 * sum(
+ int(part or 0) * mult for part, mult in zip(ts.groups(), (3600_000, 60_000, 1000, 1)))
def _format_ts(ts):
Convert an MPEG PES timestamp into a WebVTT timestamp.
This will lose sub-millisecond precision.
"""
-
- ts = int((ts + 45) // 90)
- ms , ts = divmod(ts, 1000) # noqa: W504,E221,E222,E203
- s , ts = divmod(ts, 60) # noqa: W504,E221,E222,E203
- min, h = divmod(ts, 60) # noqa: W504,E221,E222
- return '%02u:%02u:%02u.%03u' % (h, min, s, ms)
+ return '%02u:%02u:%02u.%03u' % timetuple_from_msec(int((ts + 45) // 90))
-class Block(object):
+class Block:
"""
An abstract WebVTT block.
"""
_REGEX_TSMAP = re.compile(r'X-TIMESTAMP-MAP=')
_REGEX_TSMAP_LOCAL = re.compile(r'LOCAL:')
_REGEX_TSMAP_MPEGTS = re.compile(r'MPEGTS:([0-9]+)')
+ _REGEX_TSMAP_SEP = re.compile(r'[ \t]*,[ \t]*')
@classmethod
def __parse_tsmap(cls, parser):
raise ParseError(parser)
else:
raise ParseError(parser)
- if parser.consume(','):
+ if parser.consume(cls._REGEX_TSMAP_SEP):
continue
if parser.consume(_REGEX_NL):
break
'settings': self.settings,
}
+ def __eq__(self, other):
+ return self.as_json == other.as_json
+
+ @classmethod
+ def from_json(cls, json):
+ return cls(
+ id=json['id'],
+ start=json['start'],
+ end=json['end'],
+ text=json['text'],
+ settings=json['settings']
+ )
+
+ def hinges(self, other):
+ if self.text != other.text:
+ return False
+ if self.settings != other.settings:
+ return False
+ return self.start <= self.end == other.start <= other.end
+
def parse_fragment(frag_content):
"""
a bytes object containing the raw contents of a WebVTT file.
"""
- parser = _MatchParser(frag_content.decode('utf-8'))
+ parser = _MatchParser(frag_content.decode())
yield Magic.parse(parser)