plexflow 0.0.64__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. plexflow/__init__.py +0 -0
  2. plexflow/__main__.py +15 -0
  3. plexflow/core/.DS_Store +0 -0
  4. plexflow/core/__init__.py +0 -0
  5. plexflow/core/context/__init__.py +0 -0
  6. plexflow/core/context/metadata/__init__.py +0 -0
  7. plexflow/core/context/metadata/context.py +32 -0
  8. plexflow/core/context/metadata/tmdb/__init__.py +0 -0
  9. plexflow/core/context/metadata/tmdb/context.py +45 -0
  10. plexflow/core/context/partial_context.py +46 -0
  11. plexflow/core/context/partials/__init__.py +8 -0
  12. plexflow/core/context/partials/cache.py +16 -0
  13. plexflow/core/context/partials/context.py +12 -0
  14. plexflow/core/context/partials/ids.py +37 -0
  15. plexflow/core/context/partials/movie.py +115 -0
  16. plexflow/core/context/partials/tgx_batch.py +33 -0
  17. plexflow/core/context/partials/tgx_context.py +34 -0
  18. plexflow/core/context/partials/torrents.py +23 -0
  19. plexflow/core/context/partials/watchlist.py +35 -0
  20. plexflow/core/context/plexflow_context.py +29 -0
  21. plexflow/core/context/plexflow_property.py +36 -0
  22. plexflow/core/context/root/__init__.py +0 -0
  23. plexflow/core/context/root/context.py +25 -0
  24. plexflow/core/context/select/__init__.py +0 -0
  25. plexflow/core/context/select/context.py +45 -0
  26. plexflow/core/context/torrent/__init__.py +0 -0
  27. plexflow/core/context/torrent/context.py +43 -0
  28. plexflow/core/context/torrent/tpb/__init__.py +0 -0
  29. plexflow/core/context/torrent/tpb/context.py +45 -0
  30. plexflow/core/context/torrent/yts/__init__.py +0 -0
  31. plexflow/core/context/torrent/yts/context.py +45 -0
  32. plexflow/core/context/watchlist/__init__.py +0 -0
  33. plexflow/core/context/watchlist/context.py +46 -0
  34. plexflow/core/downloads/__init__.py +0 -0
  35. plexflow/core/downloads/candidates/__init__.py +0 -0
  36. plexflow/core/downloads/candidates/download_candidate.py +210 -0
  37. plexflow/core/downloads/candidates/filtered.py +51 -0
  38. plexflow/core/downloads/candidates/utils.py +39 -0
  39. plexflow/core/env/__init__.py +0 -0
  40. plexflow/core/env/env.py +31 -0
  41. plexflow/core/genai/__init__.py +0 -0
  42. plexflow/core/genai/bot.py +9 -0
  43. plexflow/core/genai/plexa.py +54 -0
  44. plexflow/core/genai/torrent/imdb_verify.py +65 -0
  45. plexflow/core/genai/torrent/movie.py +25 -0
  46. plexflow/core/genai/utils/__init__.py +0 -0
  47. plexflow/core/genai/utils/loader.py +5 -0
  48. plexflow/core/metadata/__init__.py +0 -0
  49. plexflow/core/metadata/auto/__init__.py +0 -0
  50. plexflow/core/metadata/auto/auto_meta.py +40 -0
  51. plexflow/core/metadata/auto/auto_providers/__init__.py +0 -0
  52. plexflow/core/metadata/auto/auto_providers/auto/__init__.py +0 -0
  53. plexflow/core/metadata/auto/auto_providers/auto/episode.py +49 -0
  54. plexflow/core/metadata/auto/auto_providers/auto/item.py +55 -0
  55. plexflow/core/metadata/auto/auto_providers/auto/movie.py +13 -0
  56. plexflow/core/metadata/auto/auto_providers/auto/season.py +43 -0
  57. plexflow/core/metadata/auto/auto_providers/auto/show.py +26 -0
  58. plexflow/core/metadata/auto/auto_providers/imdb/__init__.py +0 -0
  59. plexflow/core/metadata/auto/auto_providers/imdb/movie.py +36 -0
  60. plexflow/core/metadata/auto/auto_providers/imdb/show.py +45 -0
  61. plexflow/core/metadata/auto/auto_providers/moviemeter/__init__.py +0 -0
  62. plexflow/core/metadata/auto/auto_providers/moviemeter/movie.py +40 -0
  63. plexflow/core/metadata/auto/auto_providers/plex/__init__.py +0 -0
  64. plexflow/core/metadata/auto/auto_providers/plex/movie.py +39 -0
  65. plexflow/core/metadata/auto/auto_providers/tmdb/__init__.py +0 -0
  66. plexflow/core/metadata/auto/auto_providers/tmdb/episode.py +30 -0
  67. plexflow/core/metadata/auto/auto_providers/tmdb/movie.py +36 -0
  68. plexflow/core/metadata/auto/auto_providers/tmdb/season.py +23 -0
  69. plexflow/core/metadata/auto/auto_providers/tmdb/show.py +41 -0
  70. plexflow/core/metadata/auto/auto_providers/tmdb.py +92 -0
  71. plexflow/core/metadata/auto/auto_providers/tvdb/__init__.py +0 -0
  72. plexflow/core/metadata/auto/auto_providers/tvdb/episode.py +28 -0
  73. plexflow/core/metadata/auto/auto_providers/tvdb/movie.py +36 -0
  74. plexflow/core/metadata/auto/auto_providers/tvdb/season.py +25 -0
  75. plexflow/core/metadata/auto/auto_providers/tvdb/show.py +41 -0
  76. plexflow/core/metadata/providers/__init__.py +0 -0
  77. plexflow/core/metadata/providers/imdb/__init__.py +0 -0
  78. plexflow/core/metadata/providers/imdb/datatypes.py +53 -0
  79. plexflow/core/metadata/providers/imdb/imdb.py +112 -0
  80. plexflow/core/metadata/providers/moviemeter/__init__.py +0 -0
  81. plexflow/core/metadata/providers/moviemeter/datatypes.py +111 -0
  82. plexflow/core/metadata/providers/moviemeter/moviemeter.py +42 -0
  83. plexflow/core/metadata/providers/plex/__init__.py +0 -0
  84. plexflow/core/metadata/providers/plex/datatypes.py +693 -0
  85. plexflow/core/metadata/providers/plex/plex.py +167 -0
  86. plexflow/core/metadata/providers/tmdb/__init__.py +0 -0
  87. plexflow/core/metadata/providers/tmdb/datatypes.py +460 -0
  88. plexflow/core/metadata/providers/tmdb/tmdb.py +85 -0
  89. plexflow/core/metadata/providers/tvdb/__init__.py +0 -0
  90. plexflow/core/metadata/providers/tvdb/datatypes.py +257 -0
  91. plexflow/core/metadata/providers/tvdb/tv_datatypes.py +554 -0
  92. plexflow/core/metadata/providers/tvdb/tvdb.py +65 -0
  93. plexflow/core/metadata/providers/universal/__init__.py +0 -0
  94. plexflow/core/metadata/providers/universal/movie.py +130 -0
  95. plexflow/core/metadata/providers/universal/old.py +192 -0
  96. plexflow/core/metadata/providers/universal/show.py +107 -0
  97. plexflow/core/plex/__init__.py +0 -0
  98. plexflow/core/plex/api/context/authorized.py +15 -0
  99. plexflow/core/plex/api/context/discover.py +14 -0
  100. plexflow/core/plex/api/context/library.py +14 -0
  101. plexflow/core/plex/discover/__init__.py +0 -0
  102. plexflow/core/plex/discover/activity.py +448 -0
  103. plexflow/core/plex/discover/comment.py +89 -0
  104. plexflow/core/plex/discover/feed.py +11 -0
  105. plexflow/core/plex/hooks/__init__.py +0 -0
  106. plexflow/core/plex/hooks/plex_authorized.py +60 -0
  107. plexflow/core/plex/hooks/plexflow_database.py +6 -0
  108. plexflow/core/plex/library/__init__.py +0 -0
  109. plexflow/core/plex/library/library.py +103 -0
  110. plexflow/core/plex/token/__init__.py +0 -0
  111. plexflow/core/plex/token/auto_token.py +91 -0
  112. plexflow/core/plex/utils/__init__.py +0 -0
  113. plexflow/core/plex/utils/paginated.py +39 -0
  114. plexflow/core/plex/watchlist/__init__.py +0 -0
  115. plexflow/core/plex/watchlist/datatypes.py +124 -0
  116. plexflow/core/plex/watchlist/watchlist.py +23 -0
  117. plexflow/core/storage/__init__.py +0 -0
  118. plexflow/core/storage/object/__init__.py +0 -0
  119. plexflow/core/storage/object/plexflow_storage.py +143 -0
  120. plexflow/core/storage/object/redis_storage.py +169 -0
  121. plexflow/core/subtitles/__init__.py +0 -0
  122. plexflow/core/subtitles/providers/__init__.py +0 -0
  123. plexflow/core/subtitles/providers/auto_subtitles.py +48 -0
  124. plexflow/core/subtitles/providers/oss/__init__.py +0 -0
  125. plexflow/core/subtitles/providers/oss/datatypes.py +104 -0
  126. plexflow/core/subtitles/providers/oss/download.py +48 -0
  127. plexflow/core/subtitles/providers/oss/old.py +144 -0
  128. plexflow/core/subtitles/providers/oss/oss.py +400 -0
  129. plexflow/core/subtitles/providers/oss/oss_subtitle.py +32 -0
  130. plexflow/core/subtitles/providers/oss/search.py +52 -0
  131. plexflow/core/subtitles/providers/oss/unlimited_oss.py +231 -0
  132. plexflow/core/subtitles/providers/oss/utils/__init__.py +0 -0
  133. plexflow/core/subtitles/providers/oss/utils/config.py +63 -0
  134. plexflow/core/subtitles/providers/oss/utils/download_client.py +22 -0
  135. plexflow/core/subtitles/providers/oss/utils/exceptions.py +35 -0
  136. plexflow/core/subtitles/providers/oss/utils/file_utils.py +83 -0
  137. plexflow/core/subtitles/providers/oss/utils/languages.py +78 -0
  138. plexflow/core/subtitles/providers/oss/utils/response_base.py +221 -0
  139. plexflow/core/subtitles/providers/oss/utils/responses.py +176 -0
  140. plexflow/core/subtitles/providers/oss/utils/srt.py +561 -0
  141. plexflow/core/subtitles/results/__init__.py +0 -0
  142. plexflow/core/subtitles/results/subtitle.py +170 -0
  143. plexflow/core/torrents/__init__.py +0 -0
  144. plexflow/core/torrents/analyzers/analyzed_torrent.py +143 -0
  145. plexflow/core/torrents/analyzers/analyzer.py +45 -0
  146. plexflow/core/torrents/analyzers/torrentquest/analyzer.py +47 -0
  147. plexflow/core/torrents/auto/auto_providers/auto/__init__.py +0 -0
  148. plexflow/core/torrents/auto/auto_providers/auto/torrent.py +64 -0
  149. plexflow/core/torrents/auto/auto_providers/tpb/torrent.py +62 -0
  150. plexflow/core/torrents/auto/auto_torrents.py +29 -0
  151. plexflow/core/torrents/providers/__init__.py +0 -0
  152. plexflow/core/torrents/providers/ext/__init__.py +0 -0
  153. plexflow/core/torrents/providers/ext/ext.py +18 -0
  154. plexflow/core/torrents/providers/ext/utils.py +64 -0
  155. plexflow/core/torrents/providers/extratorrent/__init__.py +0 -0
  156. plexflow/core/torrents/providers/extratorrent/extratorrent.py +21 -0
  157. plexflow/core/torrents/providers/extratorrent/utils.py +66 -0
  158. plexflow/core/torrents/providers/eztv/__init__.py +0 -0
  159. plexflow/core/torrents/providers/eztv/eztv.py +47 -0
  160. plexflow/core/torrents/providers/eztv/utils.py +83 -0
  161. plexflow/core/torrents/providers/rarbg2/__init__.py +0 -0
  162. plexflow/core/torrents/providers/rarbg2/rarbg2.py +19 -0
  163. plexflow/core/torrents/providers/rarbg2/utils.py +76 -0
  164. plexflow/core/torrents/providers/snowfl/__init__.py +0 -0
  165. plexflow/core/torrents/providers/snowfl/snowfl.py +36 -0
  166. plexflow/core/torrents/providers/snowfl/utils.py +59 -0
  167. plexflow/core/torrents/providers/tgx/__init__.py +0 -0
  168. plexflow/core/torrents/providers/tgx/context.py +50 -0
  169. plexflow/core/torrents/providers/tgx/dump.py +40 -0
  170. plexflow/core/torrents/providers/tgx/tgx.py +22 -0
  171. plexflow/core/torrents/providers/tgx/utils.py +61 -0
  172. plexflow/core/torrents/providers/therarbg/__init__.py +0 -0
  173. plexflow/core/torrents/providers/therarbg/therarbg.py +17 -0
  174. plexflow/core/torrents/providers/therarbg/utils.py +61 -0
  175. plexflow/core/torrents/providers/torrentquest/__init__.py +0 -0
  176. plexflow/core/torrents/providers/torrentquest/torrentquest.py +20 -0
  177. plexflow/core/torrents/providers/torrentquest/utils.py +70 -0
  178. plexflow/core/torrents/providers/tpb/__init__.py +0 -0
  179. plexflow/core/torrents/providers/tpb/tpb.py +17 -0
  180. plexflow/core/torrents/providers/tpb/utils.py +139 -0
  181. plexflow/core/torrents/providers/yts/__init__.py +0 -0
  182. plexflow/core/torrents/providers/yts/utils.py +57 -0
  183. plexflow/core/torrents/providers/yts/yts.py +31 -0
  184. plexflow/core/torrents/results/__init__.py +0 -0
  185. plexflow/core/torrents/results/torrent.py +165 -0
  186. plexflow/core/torrents/results/universal.py +220 -0
  187. plexflow/core/torrents/results/utils.py +15 -0
  188. plexflow/events/__init__.py +0 -0
  189. plexflow/events/download/__init__.py +0 -0
  190. plexflow/events/download/torrent_events.py +96 -0
  191. plexflow/events/publish/__init__.py +0 -0
  192. plexflow/events/publish/publish.py +34 -0
  193. plexflow/logging/__init__.py +0 -0
  194. plexflow/logging/log_setup.py +8 -0
  195. plexflow/spiders/quiet_logger.py +9 -0
  196. plexflow/spiders/tgx/pipelines/dump_json_pipeline.py +30 -0
  197. plexflow/spiders/tgx/pipelines/meta_pipeline.py +13 -0
  198. plexflow/spiders/tgx/pipelines/publish_pipeline.py +14 -0
  199. plexflow/spiders/tgx/pipelines/torrent_info_pipeline.py +12 -0
  200. plexflow/spiders/tgx/pipelines/validation_pipeline.py +17 -0
  201. plexflow/spiders/tgx/settings.py +36 -0
  202. plexflow/spiders/tgx/spider.py +72 -0
  203. plexflow/utils/__init__.py +0 -0
  204. plexflow/utils/antibot/human_like_requests.py +122 -0
  205. plexflow/utils/api/__init__.py +0 -0
  206. plexflow/utils/api/context/http.py +62 -0
  207. plexflow/utils/api/rest/__init__.py +0 -0
  208. plexflow/utils/api/rest/antibot_restful.py +68 -0
  209. plexflow/utils/api/rest/restful.py +49 -0
  210. plexflow/utils/captcha/__init__.py +0 -0
  211. plexflow/utils/captcha/bypass/__init__.py +0 -0
  212. plexflow/utils/captcha/bypass/decode_audio.py +34 -0
  213. plexflow/utils/download/__init__.py +0 -0
  214. plexflow/utils/download/gz.py +26 -0
  215. plexflow/utils/filesystem/__init__.py +0 -0
  216. plexflow/utils/filesystem/search.py +129 -0
  217. plexflow/utils/gmail/__init__.py +0 -0
  218. plexflow/utils/gmail/mails.py +116 -0
  219. plexflow/utils/hooks/__init__.py +0 -0
  220. plexflow/utils/hooks/http.py +84 -0
  221. plexflow/utils/hooks/postgresql.py +93 -0
  222. plexflow/utils/hooks/redis.py +112 -0
  223. plexflow/utils/image/storage.py +36 -0
  224. plexflow/utils/imdb/__init__.py +0 -0
  225. plexflow/utils/imdb/imdb_codes.py +107 -0
  226. plexflow/utils/pubsub/consume.py +82 -0
  227. plexflow/utils/pubsub/produce.py +25 -0
  228. plexflow/utils/retry/__init__.py +0 -0
  229. plexflow/utils/retry/utils.py +38 -0
  230. plexflow/utils/strings/__init__.py +0 -0
  231. plexflow/utils/strings/filesize.py +55 -0
  232. plexflow/utils/strings/language.py +14 -0
  233. plexflow/utils/subtitle/search.py +76 -0
  234. plexflow/utils/tasks/decorators.py +78 -0
  235. plexflow/utils/tasks/k8s/task.py +70 -0
  236. plexflow/utils/thread_safe/safe_list.py +54 -0
  237. plexflow/utils/thread_safe/safe_set.py +69 -0
  238. plexflow/utils/torrent/__init__.py +0 -0
  239. plexflow/utils/torrent/analyze.py +118 -0
  240. plexflow/utils/torrent/extract/common.py +37 -0
  241. plexflow/utils/torrent/extract/ext.py +2391 -0
  242. plexflow/utils/torrent/extract/extratorrent.py +56 -0
  243. plexflow/utils/torrent/extract/kat.py +1581 -0
  244. plexflow/utils/torrent/extract/tgx.py +96 -0
  245. plexflow/utils/torrent/extract/therarbg.py +170 -0
  246. plexflow/utils/torrent/extract/torrentquest.py +171 -0
  247. plexflow/utils/torrent/files.py +36 -0
  248. plexflow/utils/torrent/hash.py +90 -0
  249. plexflow/utils/transcribe/__init__.py +0 -0
  250. plexflow/utils/transcribe/speech2text.py +40 -0
  251. plexflow/utils/video/__init__.py +0 -0
  252. plexflow/utils/video/subtitle.py +73 -0
  253. plexflow-0.0.64.dist-info/METADATA +71 -0
  254. plexflow-0.0.64.dist-info/RECORD +256 -0
  255. plexflow-0.0.64.dist-info/WHEEL +4 -0
  256. plexflow-0.0.64.dist-info/entry_points.txt +24 -0
@@ -0,0 +1,561 @@
1
+ from __future__ import unicode_literals
2
+ import functools
3
+ import re
4
+ from datetime import timedelta
5
+ import logging
6
+ import io
7
+
8
+
9
+ LOG = logging.getLogger(__name__)
10
+
11
+ # "." is not technically valid as a delimiter, but many editors create SRT
12
+ # files with this delimiter for whatever reason. Many editors and players
13
+ # accept it, so we do too.
14
+ RGX_TIMESTAMP_MAGNITUDE_DELIM = r"[,.:,.。:]"
15
+ RGX_TIMESTAMP_FIELD = r"[0-9]+"
16
+ RGX_TIMESTAMP_FIELD_OPTIONAL = r"[0-9]*"
17
+ RGX_TIMESTAMP = "".join(
18
+ [
19
+ RGX_TIMESTAMP_MAGNITUDE_DELIM.join([RGX_TIMESTAMP_FIELD] * 3),
20
+ RGX_TIMESTAMP_MAGNITUDE_DELIM,
21
+ "?",
22
+ RGX_TIMESTAMP_FIELD_OPTIONAL,
23
+ ]
24
+ )
25
+ RGX_TIMESTAMP_PARSEABLE = r"^{}$".format(
26
+ "".join(
27
+ [
28
+ RGX_TIMESTAMP_MAGNITUDE_DELIM.join(["(" + RGX_TIMESTAMP_FIELD + ")"] * 3),
29
+ RGX_TIMESTAMP_MAGNITUDE_DELIM,
30
+ "?",
31
+ "(",
32
+ RGX_TIMESTAMP_FIELD_OPTIONAL,
33
+ ")",
34
+ ]
35
+ )
36
+ )
37
+ RGX_INDEX = r"-?[0-9]+\.?[0-9]*"
38
+ RGX_PROPRIETARY = r"[^\r\n]*"
39
+ RGX_CONTENT = r".*?"
40
+ RGX_POSSIBLE_CRLF = r"\r?\n"
41
+
42
+ TS_REGEX = re.compile(RGX_TIMESTAMP_PARSEABLE)
43
+ MULTI_WS_REGEX = re.compile(r"\n\n+")
44
+ SRT_REGEX = re.compile(
45
+ r"\s*(?:({idx})\s*{eof})?({ts}) *-[ -] *> *({ts}) ?({proprietary})(?:{eof}|\Z)({content})"
46
+ # Many sub editors don't add a blank line to the end, and many editors and
47
+ # players accept that. We allow it to be missing in input.
48
+ #
49
+ # We also allow subs that are missing a double blank newline. This often
50
+ # happens on subs which were first created as a mixed language subtitle,
51
+ # for example chs/eng, and then were stripped using naive methods (such as
52
+ # ed/sed) that don't understand newline preservation rules in SRT files.
53
+ #
54
+ # This means that when you are, say, only keeping chs, and the line only
55
+ # contains english, you end up with not only no content, but also all of
56
+ # the content lines are stripped instead of retaining a newline.
57
+ r"(?:{eof}|\Z)(?:{eof}|\Z|(?=(?:{idx}\s*{eof}{ts})))"
58
+ # Some SRT blocks, while this is technically invalid, have blank lines
59
+ # inside the subtitle content. We look ahead a little to check that the
60
+ # next lines look like an index and a timestamp as a best-effort
61
+ # solution to work around these.
62
+ r"(?=(?:(?:{idx}\s*{eof})?{ts}|\Z))".format(
63
+ idx=RGX_INDEX,
64
+ ts=RGX_TIMESTAMP,
65
+ proprietary=RGX_PROPRIETARY,
66
+ content=RGX_CONTENT,
67
+ eof=RGX_POSSIBLE_CRLF,
68
+ ),
69
+ re.DOTALL,
70
+ )
71
+
72
+ ZERO_TIMEDELTA = timedelta(0)
73
+
74
+ # Info message if truthy return -> Function taking a Subtitle, skip if True
75
+ SUBTITLE_SKIP_CONDITIONS = (
76
+ ("No content", lambda sub: not sub.content.strip()),
77
+ ("Start time < 0 seconds", lambda sub: sub.start < ZERO_TIMEDELTA),
78
+ ("Subtitle start time >= end time", lambda sub: sub.start >= sub.end),
79
+ )
80
+
81
+ SECONDS_IN_HOUR = 3600
82
+ SECONDS_IN_MINUTE = 60
83
+ HOURS_IN_DAY = 24
84
+ MICROSECONDS_IN_MILLISECOND = 1000
85
+
86
+ try:
87
+ FILE_TYPES = (file, io.IOBase) # pytype: disable=name-error
88
+ except NameError: # `file` doesn't exist in Python 3
89
+ FILE_TYPES = (io.IOBase,)
90
+
91
+
92
+ @functools.total_ordering
93
+ class Subtitle(object):
94
+ r"""
95
+ The metadata relating to a single subtitle. Subtitles are sorted by start time by default.
96
+
97
+ :param index: The SRT index for this subtitle
98
+ :type index: int or None
99
+ :param start: The time that the subtitle should start being shown
100
+ :type start: :py:class:`datetime.timedelta`
101
+ :param end: The time that the subtitle should stop being shown
102
+ :type end: :py:class:`datetime.timedelta`
103
+ :param str proprietary: Proprietary metadata for this subtitle
104
+ :param str content: The subtitle content. Should not contain OS-specific
105
+ line separators, only \\n. This is taken care of
106
+ already if you use :py:func:`srt.parse` to generate
107
+ Subtitle objects.
108
+ """
109
+
110
+ # pylint: disable=R0913
111
+ def __init__(self, index, start, end, content, proprietary=""):
112
+ """
113
+ Initialize a SubtitleEntry object with the provided data.
114
+
115
+ Args:
116
+ index (int): The index of the subtitle entry.
117
+ start (str): The start time of the subtitle entry.
118
+ end (str): The end time of the subtitle entry.
119
+ content (str): The content of the subtitle entry.
120
+ proprietary (str, optional): Proprietary information associated with the subtitle entry.
121
+
122
+ Returns:
123
+ None
124
+ """
125
+ self.index = index
126
+ self.start = start
127
+ self.end = end
128
+ self.content = content
129
+ self.proprietary = proprietary
130
+
131
+ def __hash__(self):
132
+ """
133
+ Compute the hash value for the SubtitleEntry object based on its attributes.
134
+
135
+ Returns:
136
+ int: The hash value of the object.
137
+ """
138
+ return hash(frozenset(vars(self).items()))
139
+
140
+ def __eq__(self, other):
141
+ """
142
+ Compare two SubtitleEntry objects for equality based on their attributes.
143
+
144
+ Args:
145
+ other (SubtitleEntry): The other SubtitleEntry object to compare to.
146
+
147
+ Returns:
148
+ bool: True if the objects are equal, False otherwise.
149
+ """
150
+ return vars(self) == vars(other)
151
+
152
+ def __lt__(self, other):
153
+ """
154
+ Compare two SubtitleEntry objects to determine their relative order.
155
+
156
+ Args:
157
+ other (SubtitleEntry): The other SubtitleEntry object to compare to.
158
+
159
+ Returns:
160
+ bool: True if self is less than other, False otherwise.
161
+ """
162
+ return (self.start, self.end, self.index) < (
163
+ other.start,
164
+ other.end,
165
+ other.index,
166
+ )
167
+
168
+ def __repr__(self):
169
+ """
170
+ Return a string representation of the SubtitleEntry object.
171
+
172
+ Returns:
173
+ str: A string representation of the object.
174
+ """
175
+ # Python 2/3 cross compatibility
176
+ var_items = vars(self).items() if hasattr(vars(self), "items") else vars(self).iteritems()
177
+ item_list = ", ".join("%s=%r" % (k, v) for k, v in var_items)
178
+ return "%s(%s)" % (type(self).__name__, item_list)
179
+ # var_items = getattr(vars(self), "iteritems", getattr(vars(self), "items"))
180
+ # item_list = ", ".join("%s=%r" % (k, v) for k, v in var_items())
181
+ # return "%s(%s)" % (type(self).__name__, item_list)
182
+
183
+ def to_srt(self, strict=True, eol="\n"):
184
+ r"""
185
+ Convert the current :py:class:`Subtitle` to an SRT block.
186
+
187
+ :param bool strict: If disabled, will allow blank lines in the content
188
+ of the SRT block, which is a violation of the SRT
189
+ standard and may cause your media player to explode
190
+ :param str eol: The end of line string to use (default "\\n")
191
+ :returns: The metadata of the current :py:class:`Subtitle` object as an
192
+ SRT formatted subtitle block
193
+ :rtype: str
194
+ """
195
+ output_content = self.content
196
+ output_proprietary = self.proprietary
197
+
198
+ if output_proprietary:
199
+ # output_proprietary is output directly next to the timestamp, so
200
+ # we need to add the space as a field delimiter.
201
+ output_proprietary = " " + output_proprietary
202
+
203
+ if strict:
204
+ output_content = make_legal_content(output_content)
205
+
206
+ if eol is None:
207
+ eol = "\n"
208
+ elif eol != "\n":
209
+ output_content = output_content.replace("\n", eol)
210
+
211
+ template = "{idx}{eol}{start} --> {end}{prop}{eol}{content}{eol}{eol}"
212
+ return template.format(
213
+ idx=self.index or 0,
214
+ start=timedelta_to_srt_timestamp(self.start),
215
+ end=timedelta_to_srt_timestamp(self.end),
216
+ prop=output_proprietary,
217
+ content=output_content,
218
+ eol=eol,
219
+ )
220
+
221
+ def words(self, lower=False, include_numbers=True):
222
+ """Extract words from a subtitle line.
223
+
224
+ Returns:
225
+ list: A list of words extracted from the subtitle line.
226
+ """
227
+ clean_text = re.sub(r"<.*?>", "", self.content) # Remove HTML tags (may be used for text formatting)
228
+ if not include_numbers:
229
+ clean_text = re.sub(r"[^a-zA-Z\s\']", " ", clean_text) # Removes any non-alphabet characters
230
+ return [w.strip().lower() if lower else w.strip() for w in clean_text.split()] # Extract word
231
+
232
+
233
+ def make_legal_content(content):
234
+ r"""
235
+ Remove illegal content from a content block.
236
+
237
+ Illegal content includes:
238
+ * Blank lines
239
+ * Starting or ending with a blank line
240
+
241
+ .. doctest::
242
+
243
+ >>> make_legal_content('\nfoo\n\nbar\n')
244
+ 'foo\nbar'
245
+
246
+ :param str content: The content to make legal
247
+ :returns: The legalised content
248
+ :rtype: srt
249
+ """
250
+ # Optimisation: Usually the content we get is legally valid. Do a quick
251
+ # check to see if we really need to do anything here. This saves time from
252
+ # generating legal_content by about 50%.
253
+ if content and content[0] != "\n" and "\n\n" not in content:
254
+ return content
255
+
256
+ legal_content = MULTI_WS_REGEX.sub("\n", content.strip("\n"))
257
+ LOG.info("Legalised content %r to %r", content, legal_content)
258
+ return legal_content
259
+
260
+
261
+ def timedelta_to_srt_timestamp(timedelta_timestamp):
262
+ r"""
263
+ Convert a :py:class:`~datetime.timedelta` to an SRT timestamp.
264
+
265
+ .. doctest::
266
+
267
+ >>> import datetime
268
+ >>> delta = datetime.timedelta(hours=1, minutes=23, seconds=4)
269
+ >>> timedelta_to_srt_timestamp(delta)
270
+ '01:23:04,000'
271
+
272
+ :param datetime.timedelta timedelta_timestamp: A datetime to convert to an
273
+ SRT timestamp
274
+ :returns: The timestamp in SRT format
275
+ :rtype: str
276
+ """
277
+ hrs, secs_remainder = divmod(timedelta_timestamp.seconds, SECONDS_IN_HOUR)
278
+ hrs += timedelta_timestamp.days * HOURS_IN_DAY
279
+ mins, secs = divmod(secs_remainder, SECONDS_IN_MINUTE)
280
+ msecs = timedelta_timestamp.microseconds // MICROSECONDS_IN_MILLISECOND
281
+ return "%02d:%02d:%02d,%03d" % (hrs, mins, secs, msecs)
282
+
283
+
284
+ def srt_timestamp_to_timedelta(timestamp):
285
+ r"""
286
+ Convert an SRT timestamp to a :py:class:`~datetime.timedelta`.
287
+
288
+ .. doctest::
289
+
290
+ >>> srt_timestamp_to_timedelta('01:23:04,000')
291
+ datetime.timedelta(seconds=4984)
292
+
293
+ :param str timestamp: A timestamp in SRT format
294
+ :returns: The timestamp as a :py:class:`~datetime.timedelta`
295
+ :rtype: datetime.timedelta
296
+ :raises TimestampParseError: If the timestamp is not parseable
297
+ """
298
+ match = TS_REGEX.match(timestamp)
299
+ if match is None:
300
+ raise TimestampParseError("Unparsable timestamp: {}".format(timestamp))
301
+ hrs, mins, secs, msecs = [int(m) if m else 0 for m in match.groups()]
302
+ return timedelta(hours=hrs, minutes=mins, seconds=secs, milliseconds=msecs)
303
+
304
+
305
+ def sort_and_reindex(subtitles, start_index=1, in_place=False, skip=True):
306
+ """
307
+ Reorder subtitles to be sorted by start time order, and rewrite the indexes to be in that same order.
308
+
309
+ This ensures that the SRT file will play in an
310
+ expected fashion after, for example, times were changed in some subtitles
311
+ and they may need to be resorted.
312
+
313
+ If skip=True, subtitles will also be skipped if they are considered not to
314
+ be useful. Currently, the conditions to be considered "not useful" are as
315
+ follows:
316
+
317
+ - Content is empty, or only whitespace
318
+ - The start time is negative
319
+ - The start time is equal to or later than the end time
320
+
321
+ .. doctest::
322
+
323
+ >>> from datetime import timedelta
324
+ >>> one = timedelta(seconds=1)
325
+ >>> two = timedelta(seconds=2)
326
+ >>> three = timedelta(seconds=3)
327
+ >>> subs = [
328
+ ... Subtitle(index=999, start=one, end=two, content='1'),
329
+ ... Subtitle(index=0, start=two, end=three, content='2'),
330
+ ... ]
331
+ >>> list(sort_and_reindex(subs)) # doctest: +ELLIPSIS
332
+ [Subtitle(...index=1...), Subtitle(...index=2...)]
333
+
334
+ :param subtitles: :py:class:`Subtitle` objects in any order
335
+ :param int start_index: The index to start from
336
+ :param bool in_place: Whether to modify subs in-place for performance
337
+ (version <=1.0.0 behaviour)
338
+ :param bool skip: Whether to skip subtitles considered not useful (see
339
+ above for rules)
340
+ :returns: The sorted subtitles
341
+ :rtype: :term:`generator` of :py:class:`Subtitle` objects
342
+ """
343
+ skipped_subs = 0
344
+ for sub_num, subtitle in enumerate(sorted(subtitles), start=start_index):
345
+ if not in_place:
346
+ subtitle = Subtitle(**vars(subtitle))
347
+
348
+ if skip:
349
+ try:
350
+ _should_skip_sub(subtitle)
351
+ except _ShouldSkipException as thrown_exc:
352
+ if subtitle.index is None:
353
+ LOG.info("Skipped subtitle with no index: %s", thrown_exc)
354
+ else:
355
+ LOG.info("Skipped subtitle at index %d: %s", subtitle.index, thrown_exc)
356
+ skipped_subs += 1
357
+ continue
358
+
359
+ subtitle.index = sub_num - skipped_subs
360
+
361
+ yield subtitle
362
+
363
+
364
+ def _should_skip_sub(subtitle):
365
+ """
366
+ Check if a subtitle should be skipped based on the rules in SUBTITLE_SKIP_CONDITIONS.
367
+
368
+ :param subtitle: A :py:class:`Subtitle` to check whether to skip
369
+ :raises _ShouldSkipException: If the subtitle should be skipped
370
+ """
371
+ for info_msg, sub_skipper in SUBTITLE_SKIP_CONDITIONS:
372
+ if sub_skipper(subtitle):
373
+ raise _ShouldSkipException(info_msg)
374
+
375
+
376
+ def parse(srt, ignore_errors=False, content_replace=None):
377
+ r'''
378
+ Convert an SRT formatted string (in Python 2, a :class:`unicode` object) to a :term:`generator` of Subtitle objects.
379
+
380
+ This function works around bugs present in many SRT files, most notably
381
+ that it is designed to not bork when presented with a blank line as part of
382
+ a subtitle's content.
383
+
384
+ .. doctest::
385
+
386
+ >>> subs = parse("""\
387
+ ... 422
388
+ ... 00:31:39,931 --> 00:31:41,931
389
+ ... Using mainly spoons,
390
+ ...
391
+ ... 423
392
+ ... 00:31:41,933 --> 00:31:43,435
393
+ ... we dig a tunnel under the city and release it into the wild.
394
+ ...
395
+ ... """)
396
+ >>> list(subs) # doctest: +ELLIPSIS
397
+ [Subtitle(...index=422...), Subtitle(...index=423...)]
398
+
399
+ :param srt: Subtitles in SRT format
400
+ :type srt: str or a file-like object
401
+ :param ignore_errors: If True, garbled SRT data will be ignored, and we'll
402
+ continue trying to parse the rest of the file,
403
+ instead of raising :py:class:`SRTParseError` and
404
+ stopping execution.
405
+ :returns: The subtitles contained in the SRT file as :py:class:`Subtitle`
406
+ objects
407
+ :rtype: :term:`generator` of :py:class:`Subtitle` objects
408
+ :raises SRTParseError: If the matches are not contiguous and
409
+ ``ignore_errors`` is False.
410
+ '''
411
+ expected_start = 0
412
+
413
+ # Transparently read files -- the whole thing is needed for regex's
414
+ # finditer
415
+ if isinstance(srt, FILE_TYPES):
416
+ srt = srt.read()
417
+
418
+ for match in SRT_REGEX.finditer(srt):
419
+ actual_start = match.start()
420
+ _check_contiguity(srt, expected_start, actual_start, ignore_errors)
421
+ raw_index, raw_start, raw_end, proprietary, content = match.groups()
422
+
423
+ # pytype sees that this is Optional[str] and thus complains that they
424
+ # can be None, but they can't realistically be None, since we're using
425
+ # finditer and all match groups are mandatory in the regex.
426
+ content = content.replace("\r\n", "\n") # pytype: disable=attribute-error
427
+
428
+ try:
429
+ raw_index = int(raw_index)
430
+ except ValueError:
431
+ # Index 123.4. Handled separately, since it's a rare case and we
432
+ # don't want to affect general performance.
433
+ #
434
+ # The pytype disable is for the same reason as content, above.
435
+ raw_index = int(raw_index.split(".")[0]) # pytype: disable=attribute-error
436
+ except TypeError:
437
+ # There's no index, so raw_index is already set to None. We'll
438
+ # handle this when rendering the subtitle with to_srt.
439
+ pass
440
+
441
+ if content_replace:
442
+ for key, value in content_replace.items():
443
+ content = content.replace(key, value)
444
+
445
+ yield Subtitle(
446
+ index=raw_index,
447
+ start=srt_timestamp_to_timedelta(raw_start),
448
+ end=srt_timestamp_to_timedelta(raw_end),
449
+ content=content,
450
+ proprietary=proprietary,
451
+ )
452
+
453
+ expected_start = match.end()
454
+
455
+ _check_contiguity(srt, expected_start, len(srt), ignore_errors)
456
+
457
+
458
+ def _check_contiguity(srt, expected_start, actual_start, warn_only):
459
+ """
460
+ Check contiguity.
461
+
462
+ If ``warn_only`` is False, raise :py:class:`SRTParseError` with diagnostic
463
+ info if expected_start does not equal actual_start. Otherwise, log a
464
+ warning.
465
+
466
+ :param str srt: The data being matched
467
+ :param int expected_start: The expected next start, as from the last
468
+ iteration's match.end()
469
+ :param int actual_start: The actual start, as from this iteration's
470
+ match.start()
471
+ :raises SRTParseError: If the matches are not contiguous and ``warn_only``
472
+ is False
473
+ """
474
+ if expected_start != actual_start:
475
+ unmatched_content = srt[expected_start:actual_start]
476
+
477
+ if expected_start == 0 and (unmatched_content.isspace() or unmatched_content == "\ufeff"):
478
+ # #50: Leading whitespace has nowhere to be captured like in an
479
+ # intermediate subtitle
480
+ return
481
+
482
+ if warn_only:
483
+ LOG.warning("Skipped Unparsable SRT data: %r", unmatched_content)
484
+ else:
485
+ raise SRTParseError(expected_start, actual_start, unmatched_content)
486
+
487
+
488
+ def compose(subtitles, reindex=True, start_index=1, strict=True, eol=None, in_place=False):
489
+ r"""
490
+ Convert an iterator of :py:class:`Subtitle` objects to a string of joined SRT blocks.
491
+
492
+ .. doctest::
493
+
494
+ >>> from datetime import timedelta
495
+ >>> start = timedelta(seconds=1)
496
+ >>> end = timedelta(seconds=2)
497
+ >>> subs = [
498
+ ... Subtitle(index=1, start=start, end=end, content='x'),
499
+ ... Subtitle(index=2, start=start, end=end, content='y'),
500
+ ... ]
501
+ >>> compose(subs) # doctest: +ELLIPSIS
502
+ '1\n00:00:01,000 --> 00:00:02,000\nx\n\n2\n00:00:01,000 --> ...'
503
+
504
+ :param subtitles: The subtitles to convert to SRT blocks
505
+ :type subtitles: :term:`iterator` of :py:class:`Subtitle` objects
506
+ :param bool reindex: Whether to reindex subtitles based on start time
507
+ :param int start_index: If reindexing, the index to start reindexing from
508
+ :param bool strict: Whether to enable strict mode, see
509
+ :py:func:`Subtitle.to_srt` for more information
510
+ :param str eol: The end of line string to use (default "\\n")
511
+ :returns: A single SRT formatted string, with each input
512
+ :py:class:`Subtitle` represented as an SRT block
513
+ :param bool in_place: Whether to reindex subs in-place for performance
514
+ (version <=1.0.0 behaviour)
515
+ :rtype: str
516
+ """
517
+ if reindex:
518
+ subtitles = sort_and_reindex(subtitles, start_index=start_index, in_place=in_place)
519
+
520
+ return "".join(subtitle.to_srt(strict=strict, eol=eol) for subtitle in subtitles)
521
+
522
+
523
+ class SRTParseError(Exception):
524
+ """
525
+ Raised when part of an SRT block could not be parsed.
526
+
527
+ :param int expected_start: The expected contiguous start index
528
+ :param int actual_start: The actual non-contiguous start index
529
+ :param str unmatched_content: The content between the expected start index
530
+ and the actual start index
531
+ """
532
+
533
+ def __init__(self, expected_start, actual_start, unmatched_content):
534
+ """
535
+ Initialize an SRTParseError exception.
536
+
537
+ Args:
538
+ expected_start (int): The expected starting character position.
539
+ actual_start (int): The actual starting character position.
540
+ unmatched_content (str): The unmatched content encountered.
541
+
542
+ Raises:
543
+ SRTParseError: An exception indicating a parsing error in an SRT subtitle file.
544
+ """
545
+ message = (
546
+ "Expected contiguous start of match or end of input at char %d, "
547
+ "but started at char %d (unmatched content: %r)" % (expected_start, actual_start, unmatched_content)
548
+ )
549
+ super(SRTParseError, self).__init__(message)
550
+
551
+ self.expected_start = expected_start
552
+ self.actual_start = actual_start
553
+ self.unmatched_content = unmatched_content
554
+
555
+
556
+ class TimestampParseError(ValueError):
557
+ """Raised when an SRT timestamp could not be parsed."""
558
+
559
+
560
+ class _ShouldSkipException(Exception):
561
+ """Raised when a subtitle should be skipped."""
File without changes