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,231 @@
1
+ import redis
2
+ import yaml
3
+ from typing import List, Dict, Optional, Iterator
4
+ from contextlib import contextmanager
5
+ from datetime import datetime
6
+ from plexflow.core.subtitles.providers.oss.oss import OpenSubtitles
7
+ from plexflow.core.subtitles.providers.oss.utils.exceptions import OpenSubtitlesDownloadQuotaReachedException
8
+ from plexflow.core.subtitles.providers.oss.utils.download_client import DownloadClient
9
+ from plexflow.core.subtitles.providers.oss.utils.languages import language_codes
10
+ from plexflow.core.subtitles.providers.oss.utils.responses import SearchResponse, Subtitle, DiscoverLatestResponse, DiscoverMostDownloadedResponse
11
+ import logging
12
+ import hashlib
13
+ import json
14
+ import logging
15
+ from plexflow.utils.hooks.redis import UniversalRedisHook
16
+ from plexflow.logging.log_setup import logger
17
+
18
+ class OpenSubtitlesManager:
19
+ """
20
+ A manager class for interacting with the OpenSubtitles API.
21
+
22
+ This class provides a convenient way to manage multiple credentials and handle the rate limits of the OpenSubtitles API.
23
+ It allows you to create an instance of the OpenSubtitles class using a valid credential, and automatically handles
24
+ credential rotation and blacklisting.
25
+
26
+ Usage:
27
+ credentials = [
28
+ {
29
+ 'user_agent': 'UserAgent1',
30
+ 'api_key': 'APIKey1',
31
+ 'username': 'Username1',
32
+ 'password': 'Password1'
33
+ },
34
+ {
35
+ 'user_agent': 'UserAgent2',
36
+ 'api_key': 'APIKey2',
37
+ 'username': 'Username2',
38
+ 'password': 'Password2'
39
+ }
40
+ ]
41
+
42
+ with OpenSubtitlesManager(credentials) as instance:
43
+ # Use the instance to make API calls
44
+ subtitles = instance.search_subtitles(...)
45
+
46
+ Args:
47
+ credentials (List[Dict[str, str]]): A list of dictionaries containing the credentials for accessing the OpenSubtitles API.
48
+ Each dictionary should contain the following keys: 'user_agent', 'api_key', 'username', 'password'.
49
+ redis_host (str, optional): The host of the Redis server used for blacklisting credentials. Defaults to 'localhost'.
50
+ redis_port (int, optional): The port of the Redis server used for blacklisting credentials. Defaults to 6379.
51
+ """
52
+ REDIS_KEY_PREFIX = "blacklist"
53
+ REDIS_DEFAULT_PORT = 6379
54
+ DEFAULT_TTL = 60*60 # 1 hour
55
+
56
+ def __init__(self, credentials: List[Dict[str, str]], redis_hook: UniversalRedisHook = None, **kwargs):
57
+ """
58
+ Initializes an instance of the OpenSubtitlesManager class.
59
+
60
+ Args:
61
+ credentials (List[Dict[str, str]]): A list of dictionaries containing the credentials for accessing the OpenSubtitles API.
62
+ Each dictionary should contain the following keys: 'user_agent', 'api_key', 'username', 'password'.
63
+ redis_host (str, optional): The host of the Redis server used for blacklisting credentials. Defaults to 'localhost'.
64
+ redis_port (int, optional): The port of the Redis server used for blacklisting credentials. Defaults to 6379.
65
+ """
66
+ self.credentials = credentials
67
+ self.redis_hook = redis_hook
68
+ self.current_instance = None
69
+ self.current_credential = None
70
+ self.ignore_blacklist = kwargs.get('ignore_blacklist', False)
71
+ self.logger = logger
72
+ self.instances = {}
73
+
74
+ @classmethod
75
+ def from_yaml(cls, yaml_file: str, redis_hook: UniversalRedisHook = None, **kwargs) -> 'OpenSubtitlesManager':
76
+ """
77
+ Creates an instance of the OpenSubtitlesManager class from a YAML file.
78
+
79
+ Args:
80
+ yaml_file (str): The path to the YAML file containing the credentials.
81
+ redis_host (str, optional): The host of the Redis server used for blacklisting credentials. Defaults to 'localhost'.
82
+ redis_port (int, optional): The port of the Redis server used for blacklisting credentials. Defaults to 6379.
83
+
84
+ Returns:
85
+ OpenSubtitlesManager: The created instance of the OpenSubtitlesManager class.
86
+ """
87
+ with open(yaml_file, 'r') as file:
88
+ credentials = yaml.safe_load(file)
89
+ return cls(credentials=credentials, redis_hook=redis_hook, **kwargs)
90
+
91
+ def __enter__(self) -> OpenSubtitles:
92
+ """
93
+ Enters the context and returns a valid instance of the OpenSubtitles class.
94
+
95
+ This context manager handles credential rotation and blacklisting.
96
+ If a OpenSubtitlesDownloadQuotaReachedException is raised inside the context manager,
97
+ the current credential will be blacklisted for a certain duration (default is 60 seconds)
98
+ and the exception will be re-raised.
99
+
100
+ Returns:
101
+ OpenSubtitles: The current valid instance of the OpenSubtitles class.
102
+ """
103
+ self.current_credential = self.get_next_available_credential()
104
+ self.current_instance = self.create_open_subtitles_instance(self.current_credential)
105
+ return self.current_instance
106
+
107
+ def __exit__(self, exc_type, exc_value, traceback):
108
+ """
109
+ Exits the context and cleans up the current instance and credential.
110
+
111
+ Args:
112
+ exc_type: The type of the exception raised, if any.
113
+ exc_value: The exception raised, if any.
114
+ traceback: The traceback of the exception raised, if any.
115
+ """
116
+ if exc_type == OpenSubtitlesDownloadQuotaReachedException:
117
+ self.blacklist_credential()
118
+ self.logger.warning(f"OpenSubtitlesDownloadQuotaReachedException occurred. Credential blacklisted for {self.DEFAULT_TTL} seconds.")
119
+ self.current_instance = None
120
+ self.current_credential = None
121
+ return False # Re-raise the exception
122
+
123
+ self.current_instance = None
124
+ self.current_credential = None
125
+
126
+
127
+ def get_next_available_credential(self) -> Dict[str, str]:
128
+ """
129
+ Returns the next available credential from the list of credentials.
130
+
131
+ Raises:
132
+ Exception: If no available credentials are found.
133
+
134
+ Returns:
135
+ Dict[str, str]: The next available credential.
136
+ """
137
+ for credential in self.credentials["items"]:
138
+ login = credential.get('login', {})
139
+ username = login.get('username')
140
+ password = login.get('password')
141
+ fields = credential.get('fields', [])
142
+ api_key = None
143
+ user_agent = None
144
+ for field in fields:
145
+ if field['name'] == 'API Key':
146
+ api_key = field['value']
147
+ elif field['name'] == 'User Agent':
148
+ user_agent = field['value']
149
+
150
+ credential_data = {
151
+ 'username': username,
152
+ 'password': password,
153
+ 'api_key': api_key,
154
+ 'user_agent': user_agent
155
+ }
156
+
157
+ if self.ignore_blacklist or not self.is_credential_blacklisted(credential_data):
158
+ return credential_data
159
+
160
+ self.logger.error("No available credentials found")
161
+ raise Exception("No available credentials found")
162
+
163
+ def create_open_subtitles_instance(self, credential: Dict[str, str]) -> OpenSubtitles:
164
+ """
165
+ Creates an instance of the OpenSubtitles class using the given credential.
166
+
167
+ Args:
168
+ credential (Dict[str, str]): The credential to use for creating the instance.
169
+
170
+ Returns:
171
+ OpenSubtitles: The created instance of the OpenSubtitles class.
172
+ """
173
+ api_key = credential.get('api_key')
174
+ user_agent = credential.get('user_agent')
175
+ username = credential.get('username')
176
+ password = credential.get('password')
177
+
178
+ self.logger.debug(f"Username: {username}, User Agent: {user_agent}")
179
+
180
+ # Check if instance already exists for the given credential
181
+ if api_key in self.instances:
182
+ return self.instances[api_key]
183
+
184
+ instance = OpenSubtitles(user_agent, api_key, redis_hook=self.redis_hook)
185
+ instance.login(username, password)
186
+
187
+ # Cache the instance for later reuse
188
+ self.instances[api_key] = instance
189
+
190
+ return instance
191
+
192
+ def is_credential_blacklisted(self, credential: Dict[str, str]) -> bool:
193
+ """
194
+ Checks if the given credential is blacklisted.
195
+
196
+ Args:
197
+ credential (Dict[str, str]): The credential to check.
198
+
199
+ Returns:
200
+ bool: True if the credential is blacklisted, False otherwise.
201
+ """
202
+ key = self.generate_credential_key(credential)
203
+ self.logger.debug(f"Generated credential key: {key}")
204
+ return self.redis_hook.get_conn().exists(key)
205
+
206
+ def blacklist_credential(self):
207
+ """
208
+ Blacklists the current credential for the specified duration.
209
+
210
+ """
211
+ key = self.generate_credential_key(self.current_credential)
212
+ self.redis_hook.get_conn().setex(key, self.DEFAULT_TTL, 'blacklisted')
213
+ self.logger.debug(f"Credential blacklisted for {self.DEFAULT_TTL} seconds: {self.current_credential}")
214
+ self.logger.debug(f"Blacklisted credential key: {key}")
215
+
216
+ def generate_credential_key(self, credential: Dict[str, str]) -> str:
217
+ """
218
+ Generates the key for the given credential.
219
+
220
+ Args:
221
+ credential (Dict[str, str]): The credential to generate the key for.
222
+
223
+ Returns:
224
+ str: The generated key.
225
+ """
226
+ credential_sorted = json.dumps(credential, sort_keys=True)
227
+ key_parts = [
228
+ self.REDIS_KEY_PREFIX,
229
+ hashlib.sha256(credential_sorted.encode()).hexdigest()
230
+ ]
231
+ return ":".join(key_parts)
@@ -0,0 +1,63 @@
1
+ import os
2
+ import json
3
+ import logging
4
+ from typing import Optional
5
+ from pathlib import Path
6
+
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ DEFAULT_CONFIG_PATH = "~/.config/opensubtitlescom/config.json"
11
+
12
+
13
+ class Config:
14
+ """Represents the configuration for the OpenSubtitles CLI."""
15
+
16
+ def __init__(self, path: Optional[Path] = None):
17
+ """Initialize the Config object.
18
+
19
+ Args:
20
+ path (Path): The path to the configuration file.
21
+
22
+ Note:
23
+ If the configuration file exists, the class is initialized with its content.
24
+ If not, the class creates the necessary directory structure for the configuration file.
25
+ """
26
+ self._path: Path = (Path(path or DEFAULT_CONFIG_PATH)).expanduser()
27
+ self.username: Optional[str] = None
28
+ self.password: Optional[str] = None
29
+ self.language: str = "en"
30
+
31
+ if self._path.exists():
32
+ data = json.loads(self._path.read_text())
33
+ self.__dict__.update(data)
34
+
35
+ def verify_config_dir(self):
36
+ """Verify the existence of the directory specified by self._path.
37
+
38
+ If the directory does not exist, attempts to create it.
39
+
40
+ Returns:
41
+ bool: True if the directory exists or is successfully created, False otherwise.
42
+ """
43
+ if self._path.exists():
44
+ return True
45
+ path = self._path.parent
46
+ try:
47
+ os.makedirs(path, exist_ok=True)
48
+ return True
49
+ except OSError as e:
50
+ if e.errno == 30:
51
+ logger.error(f"Error: {e}. Check if the file system is read-only or permissions are insufficient.")
52
+ else:
53
+ logger.error(f"Error: {e}")
54
+ return False
55
+
56
+ def save(self):
57
+ """Save the configuration to the specified file."""
58
+ if not self.verify_config_dir():
59
+ logger.error("Failed to save config.")
60
+ return False
61
+ data = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
62
+ self._path.write_text(json.dumps(data, indent=4))
63
+ return True
@@ -0,0 +1,22 @@
1
+ import requests
2
+
3
+
4
+ class DownloadClient:
5
+ """A client to download files URLs with."""
6
+
7
+ def __init__(self):
8
+ """Initialize the DownloadClient object."""
9
+ pass
10
+
11
+ def get(self, url: str) -> bytes:
12
+ """Download the subtitle referenced by url.
13
+
14
+ Args:
15
+ url: The url of the subtitle to download.
16
+
17
+ Returns:
18
+ The subtitles data in bytes.
19
+ """
20
+ download_remote_file = requests.get(url)
21
+
22
+ return download_remote_file.content
@@ -0,0 +1,35 @@
1
+ from datetime import datetime
2
+
3
+ class OpenSubtitlesException(Exception):
4
+ """Custom exception class for the OpenSubtitles wrapper."""
5
+
6
+ def __init__(self, message: str):
7
+ """
8
+ Initialize the custom exception.
9
+
10
+ :param message: exception message.
11
+ """
12
+ self.message = message
13
+
14
+
15
+ class OpenSubtitlesDownloadQuotaReachedException(Exception):
16
+ def __init__(self, message: str, reset_time: datetime):
17
+ """
18
+ Initialize the custom exception.
19
+
20
+ :param message: exception message.
21
+ """
22
+ self.message = message
23
+ self.reset_time = reset_time
24
+
25
+
26
+ class OpenSubtitlesFileException(Exception):
27
+ """Custom exception class for files operations in OpenSubtitles wrapper."""
28
+
29
+ def __init__(self, message: str):
30
+ """
31
+ Initialize the custom exception.
32
+
33
+ :param message: exception message.
34
+ """
35
+ self.message = message
@@ -0,0 +1,83 @@
1
+ import struct
2
+ import hashlib
3
+
4
+ from pathlib import Path
5
+
6
+ from plexflow.core.subtitles.providers.oss.utils.exceptions import OpenSubtitlesFileException
7
+
8
+
9
+ class FileUtils:
10
+ """Expose file utilities functions."""
11
+
12
+ def __init__(self, path: Path):
13
+ """Initialize the File object.
14
+
15
+ Args:
16
+ path: The Path of the file.
17
+ """
18
+ self.path = path
19
+
20
+ def write(self, content: bytes) -> None:
21
+ """Write bytes to a file Path.
22
+
23
+ Args:
24
+ content: The content of the file to be written.
25
+ Raises:
26
+ FileNotFoundError if the Path does not exist.
27
+ PermissionError if the filesystem permissions deny the operation.
28
+ """
29
+ self.path.write_bytes(content)
30
+
31
+ def delete(self) -> None:
32
+ """Delete a file Path.
33
+
34
+ Raises:
35
+ FileNotFoundError if the Path does not exist.
36
+ """
37
+ self.path.unlink()
38
+
39
+ def exists(self) -> bool:
40
+ """Confirm whether a file Path exists or not.
41
+
42
+ Raises:
43
+ PermissionError if the filesystem permissions deny the operation.
44
+ """
45
+ return self.path.exists()
46
+
47
+ def get_hash(self):
48
+ """Return the hash code of a file.
49
+
50
+ Original from: https://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes.
51
+
52
+ Returns:
53
+ - hash - hash code of a file
54
+ """
55
+ if not self.exists():
56
+ raise OpenSubtitlesFileException(f"File not exists: {self.path}")
57
+ size = self.path.stat().st_size
58
+ longlongformat = "q" # long long
59
+ bytesize = struct.calcsize(longlongformat)
60
+
61
+ if int(size) < 65536 * 2:
62
+ raise OpenSubtitlesFileException("SizeError")
63
+
64
+ with open(self.path, "rb") as file_obj:
65
+ hash = size
66
+ for _ in range(65536 // bytesize):
67
+ buffer = file_obj.read(bytesize)
68
+ (l_value,) = struct.unpack(longlongformat, buffer)
69
+ hash += l_value
70
+ hash = hash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
71
+
72
+ file_obj.seek(max(0, int(size) - 65536), 0)
73
+ for _ in range(65536 // bytesize):
74
+ buffer = file_obj.read(bytesize)
75
+ (l_value,) = struct.unpack(longlongformat, buffer)
76
+ hash += l_value
77
+ hash = hash & 0xFFFFFFFFFFFFFFFF
78
+
79
+ return str("%016x" % hash)
80
+
81
+ def get_md5(self):
82
+ """Return the md5 of a file."""
83
+ return hashlib.md5(self.path.read_bytes()).hexdigest()
@@ -0,0 +1,78 @@
1
+ language_codes = {
2
+ "af": "Afrikaans",
3
+ "sq": "Albanian",
4
+ "ar": "Arabic",
5
+ "an": "Aragonese",
6
+ "hy": "Armenian",
7
+ "at": "Asturian",
8
+ "eu": "Basque",
9
+ "be": "Belarusian",
10
+ "bn": "Bengali",
11
+ "bs": "Bosnian",
12
+ "br": "Breton",
13
+ "bg": "Bulgarian",
14
+ "my": "Burmese",
15
+ "ca": "Catalan",
16
+ "zh-cn": "Chinese (simplified)",
17
+ "cs": "Czech",
18
+ "da": "Danish",
19
+ "nl": "Dutch",
20
+ "en": "English",
21
+ "eo": "Esperanto",
22
+ "et": "Estonian",
23
+ "fi": "Finnish",
24
+ "fr": "French",
25
+ "ka": "Georgian",
26
+ "de": "German",
27
+ "gl": "Galician",
28
+ "el": "Greek",
29
+ "he": "Hebrew",
30
+ "hi": "Hindi",
31
+ "hr": "Croatian",
32
+ "hu": "Hungarian",
33
+ "is": "Icelandic",
34
+ "id": "Indonesian",
35
+ "it": "Italian",
36
+ "ja": "Japanese",
37
+ "kk": "Kazakh",
38
+ "km": "Khmer",
39
+ "ko": "Korean",
40
+ "lv": "Latvian",
41
+ "lt": "Lithuanian",
42
+ "lb": "Luxembourgish",
43
+ "mk": "Macedonian",
44
+ "ml": "Malayalam",
45
+ "ms": "Malay",
46
+ "ma": "Manipuri",
47
+ "mn": "Mongolian",
48
+ "no": "Norwegian",
49
+ "oc": "Occitan",
50
+ "fa": "Persian",
51
+ "pl": "Polish",
52
+ "pt-pt": "Portuguese",
53
+ "ru": "Russian",
54
+ "sr": "Serbian",
55
+ "si": "Sinhalese",
56
+ "sk": "Slovak",
57
+ "sl": "Slovenian",
58
+ "es": "Spanish",
59
+ "sw": "Swahili",
60
+ "sv": "Swedish",
61
+ "sy": "Syriac",
62
+ "ta": "Tamil",
63
+ "te": "Telugu",
64
+ "tl": "Tagalog",
65
+ "th": "Thai",
66
+ "tr": "Turkish",
67
+ "uk": "Ukrainian",
68
+ "ur": "Urdu",
69
+ "uz": "Uzbek",
70
+ "vi": "Vietnamese",
71
+ "ro": "Romanian",
72
+ "pt-br": "Portuguese (Brazilian)",
73
+ "me": "Montenegrin",
74
+ "zh-tw": "Chinese (traditional)",
75
+ "ze": "Chinese bilingual",
76
+ "nb": "Norwegian Bokmal",
77
+ "se": "Northern Sami",
78
+ }