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,400 @@
1
+ import json
2
+ import uuid
3
+ import logging
4
+ import requests
5
+ from pathlib import Path
6
+
7
+ from typing import Literal, Union, Optional
8
+
9
+ from plexflow.core.subtitles.providers.oss.utils.srt import parse
10
+ from plexflow.core.subtitles.providers.oss.utils.config import Config
11
+ from plexflow.core.subtitles.providers.oss.utils.file_utils import FileUtils
12
+ from plexflow.core.subtitles.providers.oss.utils.exceptions import OpenSubtitlesException, OpenSubtitlesDownloadQuotaReachedException
13
+ from plexflow.core.subtitles.providers.oss.utils.responses import (
14
+ SearchResponse,
15
+ DownloadResponse,
16
+ Subtitle,
17
+ DiscoverLatestResponse,
18
+ DiscoverMostDownloadedResponse,
19
+ )
20
+ from datetime import datetime
21
+ from plexflow.core.subtitles.providers.oss.utils.download_client import DownloadClient
22
+ from plexflow.core.subtitles.providers.oss.utils.languages import language_codes
23
+ from plexflow.utils.hooks.redis import UniversalRedisHook
24
+ from plexflow.logging.log_setup import logger
25
+
26
+ class OpenSubtitles:
27
+ """OpenSubtitles REST API Wrapper."""
28
+
29
+ def __init__(self, user_agent: str, api_key: str, redis_hook: UniversalRedisHook = None):
30
+ """Initialize the OpenSubtitles object.
31
+
32
+ :param api_key:
33
+ :param user_agent: a string representing the user agent, like: "MyApp v0.0.1"
34
+ """
35
+ self._config = Config()
36
+ self.download_client = DownloadClient()
37
+ self.base_url = "https://api.opensubtitles.com/api/v1"
38
+ self.token = None
39
+ self.api_key = api_key
40
+ self.user_agent = user_agent
41
+ self.downloads_dir = "."
42
+ self.user_downloads_remaining = 0
43
+ self.reset_time = None
44
+ self.redis_hook = redis_hook
45
+
46
+ @property
47
+ def cached_token(self):
48
+ try:
49
+ if self.redis_hook:
50
+ return self.redis_hook.get(f"@opensubtitles/{self.api_key}/token").decode("utf-8")
51
+ else:
52
+ return None
53
+ except Exception as e:
54
+ # Handle the error here
55
+ logger.warning(f"OpenSubtitles token not in cache for {self.user_agent}")
56
+ return None
57
+
58
+ @cached_token.setter
59
+ def cached_token(self, token: str):
60
+ try:
61
+ logger.debug(f"caching token: {token}")
62
+ self.redis_hook.set(f"@opensubtitles/{self.api_key}/token", token, ex=60*60)
63
+ except Exception as e:
64
+ logger.error(f"Error setting cached token: {e}")
65
+
66
+ def send_api(
67
+ self,
68
+ cmd: str,
69
+ body: Optional[dict] = None,
70
+ method: Optional[Union[str, Literal["GET", "POST", "DELETE"]]] = None,
71
+ ) -> dict:
72
+ """Send the API request."""
73
+ headers = {
74
+ "Accept": "application/json",
75
+ "Content-Type": "application/json",
76
+ "Api-Key": self.api_key,
77
+ "User-Agent": self.user_agent,
78
+ }
79
+ if self.token:
80
+ headers["authorization"] = self.token
81
+ if not method:
82
+ method = "POST" if body else "GET"
83
+
84
+ try:
85
+ if method == "DELETE":
86
+ response = requests.delete(f"{self.base_url}/{cmd}", headers=headers)
87
+ elif method == "POST":
88
+ response = requests.post(f"{self.base_url}/{cmd}", data=json.dumps(body), headers=headers)
89
+ else:
90
+ response = requests.get(f"{self.base_url}/{cmd}", headers=headers)
91
+ if response.status_code >= 400:
92
+ logger.warning(f"API `{cmd}` failed with {response.status_code}: {response.content.decode('utf-8')}")
93
+ logger.debug(response.headers)
94
+ if response.status_code == 406:
95
+ content = response.json()
96
+ raise OpenSubtitlesDownloadQuotaReachedException(message=json.dumps(content), reset_time=datetime.strptime(content.get("reset_time_utc"), "%Y-%m-%dT%H:%M:%S.%fZ"))
97
+ response.raise_for_status()
98
+ json_response = response.json()
99
+ return json_response
100
+ except requests.exceptions.HTTPError as http_err:
101
+ raise OpenSubtitlesException(f"Failed with HTTP {http_err}: {http_err}")
102
+ except requests.exceptions.RequestException as req_err:
103
+ raise OpenSubtitlesException(f"Failed to send request: {req_err}")
104
+ except ValueError as ex:
105
+ raise OpenSubtitlesException(f"Failed to parse login JSON response: {ex}")
106
+ except Exception:
107
+ raise
108
+
109
+ def login(self, username: Optional[str] = None, password: Optional[str] = None):
110
+ """
111
+ Login request - needed to obtain session token.
112
+
113
+ Docs: https://opensubtitles.stoplight.io/docs/opensubtitles-api/73acf79accc0a-login
114
+ """
115
+ stored_token = self.cached_token
116
+ if isinstance(stored_token, str):
117
+ logger.debug(f"using cached token: {stored_token}")
118
+ self.token = stored_token
119
+ self.user_downloads_remaining = 20
120
+ else:
121
+ logger.debug('no cached token: using new token')
122
+ body = {"username": username or self._config.username, "password": password or self._config.password}
123
+ login_response = self.send_api("login", body, method="POST")
124
+ self.token = login_response["token"]
125
+ self.cached_token = self.token
126
+ self.user_downloads_remaining = login_response["user"]["allowed_downloads"]
127
+ return login_response
128
+
129
+ def logout(self, username: str, password: str):
130
+ """
131
+ Destroy a user token to end a session.
132
+
133
+ Docs: https://opensubtitles.stoplight.io/docs/opensubtitles-api/9fe4d6d078e50-logout
134
+ """
135
+ response = self.send_api("logout", method="DELETE")
136
+ return response
137
+
138
+ def user_info(self):
139
+ """
140
+ Get user data.
141
+
142
+ Docs: https://opensubtitles.stoplight.io/docs/opensubtitles-api/ea912bb244ef0-user-informations
143
+ """
144
+ response = self.send_api("infos/user")
145
+ self.user_downloads_remaining = response["data"]["remaining_downloads"]
146
+ return response
147
+
148
+ def languages_info(self):
149
+ """
150
+ Get the languages information.
151
+
152
+ Docs: https://opensubtitles.stoplight.io/docs/opensubtitles-api/1de776d20e873-languages
153
+ """
154
+ response = self.send_api("infos/languages")
155
+ return response
156
+
157
+ def formats_info(self):
158
+ """
159
+ Get the languages information.
160
+
161
+ Docs: https://opensubtitles.stoplight.io/docs/opensubtitles-api/69b286fc7506e-subtitle-formats
162
+ """
163
+ response = self.send_api("infos/formats")
164
+ return response
165
+
166
+ def discover_latest(self):
167
+ """
168
+ Get 60 latest uploaded subtitles.
169
+
170
+ Docs: https://opensubtitles.stoplight.io/docs/opensubtitles-api/f36cef28efaa9-latest-subtitles
171
+ """
172
+ response = self.send_api("discover/latest")
173
+ return DiscoverLatestResponse(**response)
174
+
175
+ def discover_most_downloaded(
176
+ self, languages: Optional[str] = None, type: Optional[Union[str, Literal["movie", "tvshow"]]] = None
177
+ ):
178
+ """
179
+ Get popular subtitles, according to last 30 days downloads on opensubtitles.com.
180
+
181
+ Docs: https://opensubtitles.stoplight.io/docs/opensubtitles-api/3a149b956fcab-most-downloaded-subtitles
182
+ """
183
+ response = self.send_api("discover/most_downloaded")
184
+ return DiscoverMostDownloadedResponse(**response)
185
+
186
+ def search(
187
+ self,
188
+ *,
189
+ ai_translated: Optional[Union[str, Literal["exclude", "include"]]] = None,
190
+ episode_number: Optional[int] = None,
191
+ foreign_parts_only: Optional[Union[str, Literal["exclude", "include"]]] = None,
192
+ hearing_impaired: Optional[Union[str, Literal["exclude", "include"]]] = None,
193
+ id: Optional[int] = None,
194
+ imdb_id: Optional[int] = None,
195
+ languages: Optional[str] = None,
196
+ machine_translated: Optional[Union[str, Literal["exclude", "include"]]] = None,
197
+ moviehash: Optional[str] = None,
198
+ moviehash_match: Optional[Union[str, Literal["include", "only"]]] = None,
199
+ order_by: Optional[str] = None,
200
+ order_direction: Optional[Union[str, Literal["asc", "desc"]]] = None,
201
+ page: Optional[int] = None,
202
+ parent_feature_id: Optional[int] = None,
203
+ parent_imdb_id: Optional[int] = None,
204
+ parent_tmdb_id: Optional[int] = None,
205
+ query: Optional[str] = None,
206
+ season_number: Optional[int] = None,
207
+ tmdb_id: Optional[int] = None,
208
+ trusted_sources: Optional[Union[str, Literal["include", "only"]]] = None,
209
+ type: Optional[Union[str, Literal["movie", "episode", "all"]]] = None,
210
+ user_id: Optional[int] = None,
211
+ year: Optional[int] = None,
212
+ ) -> SearchResponse:
213
+ """
214
+ Search for subtitles.
215
+
216
+ Docs: https://opensubtitles.stoplight.io/docs/opensubtitles-api/a172317bd5ccc-search-for-subtitles
217
+ """
218
+ query_params = []
219
+
220
+ # Helper function to add a parameter to the query_params list
221
+ def add_param(name, value):
222
+ if value is not None:
223
+ query_params.append(f"{name}={value}")
224
+
225
+ # Add parameters to the query_params list
226
+ add_param("ai_translated", ai_translated)
227
+ add_param("episode_number", episode_number)
228
+ add_param("foreign_parts_only", foreign_parts_only)
229
+ add_param("hearing_impaired", hearing_impaired)
230
+ add_param("id", id)
231
+ add_param("imdb_id", imdb_id)
232
+ add_param("languages", languages)
233
+ add_param("machine_translated", machine_translated)
234
+ add_param("moviehash", moviehash)
235
+ add_param("moviehash_match", moviehash_match)
236
+ add_param("order_by", order_by)
237
+ add_param("order_direction", order_direction)
238
+ add_param("page", page)
239
+ add_param("parent_feature_id", parent_feature_id)
240
+ add_param("parent_imdb_id", parent_imdb_id)
241
+ add_param("parent_tmdb_id", parent_tmdb_id)
242
+ add_param("query", query)
243
+ add_param("season_number", season_number)
244
+ add_param("tmdb_id", tmdb_id)
245
+ add_param("trusted_sources", trusted_sources)
246
+ add_param("type", type)
247
+ add_param("user_id", user_id)
248
+ add_param("year", year)
249
+
250
+ #if languages is not None:
251
+ # assert languages in language_codes, f"Invalid language code: {languages}"
252
+ # assert query_params, "Missing subtitles search parameters"
253
+ query_string = "&".join(query_params)
254
+
255
+ search_response_data = self.send_api(f"subtitles?{query_string}")
256
+ return SearchResponse(**search_response_data, query_string=query_string)
257
+
258
+ def download_and_parse(self, *args, **kwargs) -> list:
259
+ """
260
+ Download a subtitle file and parse it into a list of subtitle entries in SRT format.
261
+
262
+ Args:
263
+ *args: Variable-length positional arguments.
264
+ **kwargs: Variable-length keyword arguments.
265
+
266
+ Returns:
267
+ list: A list of parsed subtitle entries in SRT format.
268
+
269
+ This function first downloads a subtitle file using the provided arguments and then parses
270
+ the content of the downloaded file into a list of subtitle entries. The downloaded subtitle
271
+ file is expected to be in SRT format.
272
+
273
+ Note: You can specify various optional parameters when calling this function to control
274
+ the download and parsing process, such as sub_format, file_name, in_fps, out_fps, timeshift,
275
+ and force_download.
276
+
277
+ Example usage:
278
+ ```
279
+ subtitles = open_subtitles.download_and_parse(file_id, sub_format="srt")
280
+ for subtitle_entry in subtitles:
281
+ print(subtitle_entry.content)
282
+ ```
283
+ """
284
+ content = self.download(*args, **kwargs)
285
+ return self.parse_srt(self.bytes_to_str(content))
286
+
287
+ def download(
288
+ self,
289
+ file_id: Union[str, Subtitle],
290
+ sub_format: Optional[int] = None,
291
+ file_name: Optional[int] = None,
292
+ in_fps: Optional[int] = None,
293
+ out_fps: Optional[int] = None,
294
+ timeshift: Optional[int] = None,
295
+ force_download: Optional[bool] = None,
296
+ ) -> bytes:
297
+ """
298
+ Download a single subtitle file using the file_no.
299
+
300
+ Docs: https://opensubtitles.stoplight.io/docs/opensubtitles-api/6be7f6ae2d918-download
301
+ """
302
+ subtitle_id = file_id.file_id if isinstance(file_id, Subtitle) else file_id
303
+ if not subtitle_id:
304
+ raise OpenSubtitlesException("Missing subtitle file id.")
305
+
306
+ download_body = {"file_id": subtitle_id}
307
+
308
+ # Helper function to add a parameter to the query_params list
309
+ def add_param(name, value):
310
+ if value is not None:
311
+ download_body[name] = value
312
+
313
+ add_param("sub_format", sub_format)
314
+ add_param("file_name", file_name)
315
+ add_param("in_fps", in_fps)
316
+ add_param("out_fps", out_fps)
317
+ add_param("timeshift", timeshift)
318
+ add_param("force_download", force_download)
319
+
320
+ if self.user_downloads_remaining <= 0:
321
+ raise OpenSubtitlesDownloadQuotaReachedException(
322
+ "Download limit reached. " "Please upgrade your account or wait for your quota to reset (~24hrs)",
323
+ reset_time=self.reset_time
324
+ )
325
+
326
+ try:
327
+ search_response_data = DownloadResponse(self.send_api("download", download_body))
328
+ self.user_downloads_remaining = search_response_data.remaining
329
+ self.reset_time = search_response_data.reset_time_utc
330
+ return self.download_client.get(search_response_data.link)
331
+ except OpenSubtitlesDownloadQuotaReachedException:
332
+ raise
333
+
334
+ def save_content_locally(self, content: bytes, filename: Optional[str] = None) -> str:
335
+ """
336
+ Save content locally.
337
+
338
+ :param content: content of subtitle file.
339
+ :param filename: target local filename.
340
+ :return: the path of the local file containing the content.
341
+ """
342
+ local_filename = f"{str(filename).removesuffix('.srt') if filename else uuid.uuid4()}.srt"
343
+ srt_path = Path(self.downloads_dir).joinpath(local_filename)
344
+ FileUtils(srt_path).write(content)
345
+ return srt_path.as_posix()
346
+
347
+ def download_and_save(self, file_id: Union[str, Subtitle], **kwargs) -> str:
348
+ """Call the download function to get the subtitle content, then save the content to a local file.
349
+
350
+ :param file_id: file_id or subtitles object.
351
+ :return: local file path.
352
+ """
353
+ filename = kwargs.pop("filename", None)
354
+ subtitle_id = file_id.file_id if isinstance(file_id, Subtitle) else file_id
355
+ content = self.download(subtitle_id, **kwargs)
356
+ if not content:
357
+ raise OpenSubtitlesException(f"Failed to get content for: {file_id}")
358
+ filename = filename or subtitle_id
359
+ return self.save_content_locally(content, filename)
360
+
361
+ def parse_srt(self, content) -> list:
362
+ """
363
+ Parse subtitles in SRT format.
364
+
365
+ Args:
366
+ content (str): The content of the subtitles SRT file.
367
+
368
+ Returns:
369
+ list: A list of parsed subtitle entries.
370
+ """
371
+ parsed_content = parse(content)
372
+ return list(parsed_content)
373
+
374
+ def bytes_to_str(self, content: bytes) -> str:
375
+ """
376
+ Convert bytes content to a string.
377
+
378
+ Args:
379
+ content (bytes): The bytes content to be converted.
380
+
381
+ Returns:
382
+ str: The content as a UTF-8 encoded string.
383
+ """
384
+ if isinstance(content, bytes):
385
+ content = content.decode("utf-8")
386
+ return content
387
+
388
+ def str_to_bytes(self, content: str) -> bytes:
389
+ """
390
+ Convert string content to bytes.
391
+
392
+ Args:
393
+ content (str): The string content to be converted.
394
+
395
+ Returns:
396
+ bytes: The content as bytes, encoded in UTF-8.
397
+ """
398
+ if isinstance(content, str):
399
+ content = content.encode("utf-8")
400
+ return content
@@ -0,0 +1,32 @@
1
+ from plexflow.core.subtitles.providers.oss.utils.responses import Subtitle
2
+ from datetime import datetime
3
+ from plexflow.utils.imdb.imdb_codes import IMDbCode
4
+
5
+ class OSSSubtitle(Subtitle):
6
+ def __init__(self, subtitle: Subtitle):
7
+ self.subtitle = subtitle
8
+ self.src = "oss"
9
+
10
+ @property
11
+ def release_name(self) -> str:
12
+ return self.subtitle.release
13
+
14
+ @property
15
+ def uploader(self) -> str:
16
+ return self.subtitle.uploader_name
17
+
18
+ @property
19
+ def date(self) -> datetime:
20
+ return datetime.strptime(self.subtitle.upload_date, "%Y-%m-%dT%H:%M:%SZ")
21
+
22
+ @property
23
+ def imdb_code(self) -> IMDbCode:
24
+ return IMDbCode(str(self.subtitle.imdb_id))
25
+
26
+ @property
27
+ def subtitle_id(self) -> str:
28
+ return self.subtitle.id
29
+
30
+ @property
31
+ def language(self):
32
+ return self.subtitle.language
@@ -0,0 +1,52 @@
1
+ from typing import List
2
+ from contextlib import contextmanager
3
+ from plexflow.core.subtitles.providers.oss.unlimited_oss import OpenSubtitlesManager, Subtitle
4
+ from plexflow.core.subtitles.providers.oss.oss_subtitle import OSSSubtitle
5
+ from typing import Any, List
6
+ from contextlib import contextmanager, ExitStack
7
+ from plexflow.utils.hooks.redis import UniversalRedisHook
8
+
9
+ @contextmanager
10
+ def open_subtitles_manager(credentials_path: str, redis_hook: UniversalRedisHook = None, **kwargs: Any):
11
+ """
12
+ Context manager for managing the OpenSubtitlesManager instance.
13
+
14
+ Args:
15
+ credentials_path: The path to the YAML file containing OpenSubtitles credentials.
16
+ redis_host: The host address of the Redis server.
17
+ redis_port: The port number of the Redis server.
18
+
19
+ Yields:
20
+ OpenSubtitlesManager: The OpenSubtitlesManager instance.
21
+ """
22
+ with ExitStack() as stack:
23
+ manager = stack.enter_context(OpenSubtitlesManager.from_yaml(
24
+ yaml_file=credentials_path,
25
+ redis_hook=redis_hook,
26
+ **kwargs
27
+ ))
28
+ yield manager
29
+
30
+ def get_subtitles(imdb_id: str, languages: List[str] = (), redis_hook: UniversalRedisHook = None, ignore_blacklist: bool = False, **kwargs) -> List[OSSSubtitle]:
31
+ """
32
+ Retrieves subtitles using OpenSubtitlesManager.
33
+
34
+ Args:
35
+ imdb_id: The IMDb ID of the movie or TV show.
36
+ languages: A list of language codes for the desired subtitles.
37
+
38
+ Returns:
39
+ A list of subtitle data retrieved from OpenSubtitlesManager.
40
+ """
41
+ with open_subtitles_manager(
42
+ credentials_path=kwargs.pop("credentials_path"),
43
+ redis_hook=redis_hook,
44
+ ignore_blacklist=ignore_blacklist,
45
+ ) as manager:
46
+ subtitles = manager.search(
47
+ imdb_id=imdb_id,
48
+ languages=','.join(languages),
49
+ **kwargs
50
+ )
51
+
52
+ return list(map(OSSSubtitle, subtitles.data))