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,169 @@
1
+ import json
2
+ import pickle
3
+ import redis
4
+
5
+ class RedisObjectStore:
6
+ """A class used to represent a Redis Object Store.
7
+
8
+ Attributes:
9
+ client (Redis): A Redis client instance.
10
+ default_ttl (int): The default TTL (in seconds) for stored objects.
11
+
12
+ """
13
+
14
+ def __init__(self, host='localhost', port=6379, db=0, default_ttl=3600, password=None):
15
+ """Constructs all the necessary attributes for the RedisObjectStore object.
16
+
17
+ Args:
18
+ host (str, optional): The host of the Redis server. Defaults to 'localhost'.
19
+ port (int, optional): The port of the Redis server. Defaults to 6379.
20
+ db (int, optional): The database index of the Redis server. Defaults to 0.
21
+ default_ttl (int, optional): The default TTL (in seconds) for stored objects. Defaults to 3600.
22
+
23
+ """
24
+
25
+ self.client = redis.Redis(host=host, port=port, db=db, password=password)
26
+ self.default_ttl = default_ttl
27
+
28
+ def _serialize(self, obj, use_json=False):
29
+ """Serializes an object.
30
+
31
+ Args:
32
+ obj (object): The object to serialize.
33
+ use_json (bool, optional): Whether to use JSON for serialization. If False, pickle is used.
34
+
35
+ Returns:
36
+ str: The serialized object.
37
+
38
+ Raises:
39
+ Exception: If an error occurs during serialization.
40
+
41
+ """
42
+
43
+ try:
44
+ return json.dumps(obj) if use_json else pickle.dumps(obj)
45
+ except (TypeError, pickle.PicklingError) as e:
46
+ raise Exception("Failed to serialize object") from e
47
+
48
+ def retrieve_keys(self, pattern):
49
+ """Retrieves all keys matching a given pattern from the Redis store.
50
+
51
+ Args:
52
+ pattern (str): The pattern to match.
53
+
54
+ Returns:
55
+ list: A list of keys matching the pattern.
56
+
57
+ Raises:
58
+ Exception: If an error occurs during retrieving the keys.
59
+
60
+ """
61
+
62
+ try:
63
+ keys = self.client.keys(str(pattern))
64
+ return keys
65
+ except redis.RedisError as e:
66
+ raise Exception("Failed to retrieve keys from Redis") from e
67
+
68
+ def retrieve_values(self, pattern, use_json=False):
69
+ """Retrieves all values matching a given pattern from the Redis store and deserializes them.
70
+
71
+ Args:
72
+ pattern (str): The pattern to match.
73
+ use_json (bool, optional): Whether to use JSON for deserialization. If False, pickle is used.
74
+
75
+ Returns:
76
+ list: A list of deserialized values matching the pattern.
77
+
78
+ Raises:
79
+ Exception: If an error occurs during retrieving or deserializing the values.
80
+
81
+ """
82
+
83
+ try:
84
+ keys = self.client.keys(str(pattern))
85
+ serialized_values = self.client.mget(keys)
86
+ values = []
87
+ for serialized_value in serialized_values:
88
+ if serialized_value is not None:
89
+ value = json.loads(serialized_value) if use_json else pickle.loads(serialized_value)
90
+ values.append(value)
91
+ else:
92
+ values.append(None)
93
+ return values
94
+ except redis.RedisError as e:
95
+ raise Exception("Failed to retrieve values from Redis") from e
96
+
97
+ def _store(self, key, serialized_object, ttl=None):
98
+ """Stores a serialized object in the Redis store.
99
+
100
+ Args:
101
+ key (str): The key under which the object is stored.
102
+ serialized_object (str): The serialized object to store.
103
+ ttl (int, optional): The TTL (in seconds) for the stored object. If None, the object is stored permanently.
104
+
105
+ Raises:
106
+ Exception: If an error occurs during storing the object.
107
+
108
+ """
109
+
110
+ try:
111
+ self.client.set(key, serialized_object, ex=ttl)
112
+ except redis.RedisError as e:
113
+ raise Exception("Failed to store object in Redis") from e
114
+
115
+ def store(self, key, obj, use_json=False):
116
+ """Stores a serialized version of an object in the Redis store permanently.
117
+
118
+ Args:
119
+ key (str): The key under which the object is stored.
120
+ obj (object): The object to store.
121
+ use_json (bool, optional): Whether to use JSON for serialization. If False, pickle is used.
122
+
123
+ """
124
+
125
+ serialized_object = self._serialize(obj, use_json)
126
+ self._store(key, serialized_object)
127
+
128
+ def store_temporarily(self, key, obj, ttl=None, use_json=False):
129
+ """Stores a serialized version of an object in the Redis store temporarily.
130
+
131
+ Args:
132
+ key (str): The key under which the object is stored.
133
+ obj (object): The object to store.
134
+ ttl (int, optional): The TTL (in seconds) for the stored object. If None, the default TTL is used.
135
+ use_json (bool, optional): Whether to use JSON for serialization. If False, pickle is used.
136
+
137
+ """
138
+
139
+ serialized_object = self._serialize(obj, use_json)
140
+ ttl = self.default_ttl if ttl is None else ttl
141
+ self._store(key, serialized_object, ttl)
142
+
143
+ def retrieve(self, key, use_json=False):
144
+ """Retrieves an object from the Redis store and deserializes it.
145
+
146
+ Args:
147
+ key (str): The key of the object to retrieve.
148
+ use_json (bool, optional): Whether to use JSON for deserialization. If False, pickle is used.
149
+
150
+ Returns:
151
+ object: The retrieved and deserialized object. If the object does not exist, returns None.
152
+
153
+ Raises:
154
+ Exception: If an error occurs during retrieving or deserializing the object.
155
+
156
+ """
157
+
158
+ try:
159
+ serialized_object = self.client.get(key)
160
+ except redis.RedisError as e:
161
+ raise Exception("Failed to retrieve object from Redis") from e
162
+
163
+ if serialized_object is None:
164
+ return None
165
+
166
+ try:
167
+ return json.loads(serialized_object) if use_json else pickle.loads(serialized_object)
168
+ except (json.JSONDecodeError, pickle.UnpicklingError) as e:
169
+ raise Exception("Failed to deserialize object") from e
File without changes
File without changes
@@ -0,0 +1,48 @@
1
+ from plexflow.core.subtitles.providers.oss.search import get_subtitles, OSSSubtitle
2
+ from typing import List, Iterator, Any
3
+ from plexflow.utils.hooks.redis import UniversalRedisHook
4
+ from plexflow.core.subtitles.providers.oss.download import download_subtitle
5
+ from tqdm import tqdm
6
+ from plexflow.core.subtitles.providers.oss.unlimited_oss import OpenSubtitlesDownloadQuotaReachedException
7
+ from plexflow.utils.retry.utils import execute_until_success
8
+ import time
9
+ from pathlib import Path
10
+ from plexflow.logging.log_setup import logger
11
+
12
+ class AutoSubtitles:
13
+ """
14
+ A class that represents an auto subtitles provider.
15
+
16
+ Attributes:
17
+ imdb_id (str): The IMDb ID of the movie or TV show.
18
+ languages (List[str]): A list of languages for which subtitles are requested.
19
+ kwargs: Additional keyword arguments.
20
+
21
+ """
22
+ def __init__(self, imdb_id: str, languages: List[str] = (), **kwargs: Any) -> None:
23
+ self.imdb_id = imdb_id
24
+ self.languages = languages
25
+ self.redis_hook = kwargs.pop("redis_hook", UniversalRedisHook(redis_conn_id='redis', config_folder='config'))
26
+ self.download = kwargs.pop("download", False)
27
+ self.download_folder = Path(kwargs.pop("download_folder", Path(".")))
28
+ self.kwargs = kwargs
29
+
30
+ def __iter__(self) -> Iterator[OSSSubtitle]:
31
+ subtitles = get_subtitles(self.imdb_id, self.languages, self.redis_hook, ignore_blacklist=True, **self.kwargs)
32
+ if len(subtitles) == 0:
33
+ logger.warning(f"No subtitles found for IMDb ID: {self.imdb_id}")
34
+ return
35
+
36
+ for subtitle in tqdm(subtitles, total=len(subtitles)):
37
+ if self.download:
38
+ subtitle_path, skipped = execute_until_success(download_subtitle, delay_type='constant', delay=3, max_retries=10, subtitle=subtitle, redis_hook=self.redis_hook, retry_exceptions=[OpenSubtitlesDownloadQuotaReachedException], save_dir=self.download_folder)
39
+ if not skipped:
40
+ logger.debug(f"Subtitle downloaded: {subtitle_path}")
41
+ # time.sleep(1.2)
42
+ yield subtitle
43
+
44
+ def __next__(self) -> Any:
45
+ try:
46
+ return next(self.__iter__())
47
+ except StopIteration:
48
+ raise
File without changes
@@ -0,0 +1,104 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import List, Optional
3
+ from dataclasses_json import dataclass_json, Undefined
4
+
5
+ @dataclass_json(undefined=Undefined.EXCLUDE)
6
+ @dataclass
7
+ class Uploader:
8
+ uploader_id: Optional[int] = None
9
+ name: Optional[str] = None
10
+ rank: Optional[str] = None
11
+ _catchall: dict = field(default_factory=dict)
12
+
13
+ @dataclass_json(undefined=Undefined.EXCLUDE)
14
+ @dataclass
15
+ class FeatureDetails:
16
+ feature_id: Optional[int] = None
17
+ feature_type: Optional[str] = None
18
+ year: Optional[int] = None
19
+ title: Optional[str] = None
20
+ movie_name: Optional[str] = None
21
+ imdb_id: Optional[int] = None
22
+ tmdb_id: Optional[int] = None
23
+ _catchall: dict = field(default_factory=dict)
24
+
25
+ @dataclass_json(undefined=Undefined.EXCLUDE)
26
+ @dataclass
27
+ class RelatedLinks:
28
+ label: Optional[str] = None
29
+ url: Optional[str] = None
30
+ img_url: Optional[str] = None
31
+ _catchall: dict = field(default_factory=dict)
32
+
33
+ @dataclass_json(undefined=Undefined.EXCLUDE)
34
+ @dataclass
35
+ class Files:
36
+ file_id: Optional[int] = None
37
+ cd_number: Optional[int] = None
38
+ file_name: Optional[str] = None
39
+ _catchall: dict = field(default_factory=dict)
40
+
41
+ @dataclass_json(undefined=Undefined.EXCLUDE)
42
+ @dataclass
43
+ class Attributes:
44
+ subtitle_id: Optional[str] = None
45
+ language: Optional[str] = None
46
+ download_count: Optional[int] = None
47
+ new_download_count: Optional[int] = None
48
+ hearing_impaired: Optional[bool] = None
49
+ hd: Optional[bool] = None
50
+ fps: Optional[float] = None
51
+ votes: Optional[int] = None
52
+ ratings: Optional[float] = None
53
+ from_trusted: Optional[bool] = None
54
+ foreign_parts_only: Optional[bool] = None
55
+ upload_date: Optional[str] = None
56
+ ai_translated: Optional[bool] = None
57
+ nb_cd: Optional[int] = None
58
+ machine_translated: Optional[bool] = None
59
+ release: Optional[str] = None
60
+ comments: Optional[str] = None
61
+ legacy_subtitle_id: Optional[int] = None
62
+ legacy_uploader_id: Optional[int] = None
63
+ uploader: Optional[Uploader] = None
64
+ feature_details: Optional[FeatureDetails] = None
65
+ url: Optional[str] = None
66
+ related_links: Optional[List[RelatedLinks]] = None
67
+ files: Optional[List[Files]] = None
68
+ _catchall: dict = field(default_factory=dict)
69
+
70
+ def __post_init__(self):
71
+ if isinstance(self.uploader, dict):
72
+ self.uploader = Uploader(**self.uploader)
73
+ if isinstance(self.feature_details, dict):
74
+ self.feature_details = FeatureDetails(**self.feature_details)
75
+ if isinstance(self.related_links, list):
76
+ self.related_links = [RelatedLinks(**rl) if isinstance(rl, dict) else rl for rl in self.related_links]
77
+ if isinstance(self.files, list):
78
+ self.files = [Files(**f) if isinstance(f, dict) else f for f in self.files]
79
+
80
+ @dataclass_json(undefined=Undefined.EXCLUDE)
81
+ @dataclass
82
+ class Data:
83
+ id: Optional[str] = None
84
+ type: Optional[str] = None
85
+ attributes: Optional[Attributes] = None
86
+ _catchall: dict = field(default_factory=dict)
87
+
88
+ def __post_init__(self):
89
+ if isinstance(self.attributes, dict):
90
+ self.attributes = Attributes(**self.attributes)
91
+
92
+ @dataclass_json(undefined=Undefined.EXCLUDE)
93
+ @dataclass
94
+ class SubtitleData:
95
+ total_pages: Optional[int] = None
96
+ total_count: Optional[int] = None
97
+ per_page: Optional[int] = None
98
+ page: Optional[int] = None
99
+ data: Optional[List[Data]] = None
100
+ _catchall: dict = field(default_factory=dict)
101
+
102
+ def __post_init__(self):
103
+ if isinstance(self.data, list):
104
+ self.data = [Data(**d) if isinstance(d, dict) else d for d in self.data]
@@ -0,0 +1,48 @@
1
+ from typing import List, Any
2
+ from contextlib import contextmanager, ExitStack
3
+ from plexflow.core.subtitles.providers.oss.unlimited_oss import OpenSubtitlesManager, Subtitle, OpenSubtitlesDownloadQuotaReachedException
4
+ from plexflow.core.subtitles.providers.oss.oss_subtitle import OSSSubtitle
5
+ from plexflow.utils.retry.utils import execute_until_success
6
+ from plexflow.utils.hooks.redis import UniversalRedisHook
7
+ from pathlib import Path
8
+ from plexflow.logging.log_setup import logger
9
+
10
+ def download_subtitle(subtitle: OSSSubtitle, redis_hook: UniversalRedisHook = None, save_dir: Path = Path('.'), skip_exists: bool = True) -> None:
11
+ """
12
+ Downloads and saves the subtitle file using the OpenSubtitlesManager.
13
+
14
+ Args:
15
+ subtitle (OSSSubtitle): The subtitle object containing the file ID.
16
+
17
+ Returns:
18
+ None
19
+ """
20
+ folder = save_dir / str(subtitle.imdb_code) / subtitle.subtitle.language / str(subtitle.subtitle.id)
21
+ filepath = folder / (str(subtitle.subtitle.file_id) + ".srt")
22
+ metapath = folder / "metadata.json"
23
+
24
+ if skip_exists and filepath.exists():
25
+ logger.debug(f"Subtitle already exists: {filepath}")
26
+ return None, True
27
+ else:
28
+ with OpenSubtitlesManager.from_yaml(
29
+ yaml_file='config/credentials.yaml',
30
+ redis_hook=redis_hook,
31
+ ) as manager:
32
+ filepath.parent.mkdir(parents=True, exist_ok=True)
33
+ metapath.write_text(subtitle.subtitle.to_json())
34
+ return manager.download_and_save(subtitle.subtitle.file_id, filename=str(filepath)), False
35
+
36
+ def download_subtitles(subtitles: List[OSSSubtitle], **kwargs) -> None:
37
+ """
38
+ Downloads subtitles for a list of OSSSubtitle objects.
39
+
40
+ Args:
41
+ subtitles (List[OSSSubtitle]): A list of OSSSubtitle objects representing the subtitles to be downloaded.
42
+
43
+ Returns:
44
+ None
45
+ """
46
+ redis_hook = kwargs.pop("redis_hook", UniversalRedisHook(redis_conn_id='redis', config_folder='config'))
47
+ for subtitle in subtitles:
48
+ execute_until_success(download_subtitle, delay_type='constant', delay=3, max_retries=10, subtitle=subtitle, redis_hook=redis_hook, retry_exceptions=[OpenSubtitlesDownloadQuotaReachedException])
@@ -0,0 +1,144 @@
1
+ import json
2
+ import logging
3
+ import requests
4
+ from slugify import slugify
5
+ import time
6
+
7
+ # Set up logging
8
+ logging.basicConfig(level=logging.INFO)
9
+
10
+ class OpenSubtitles:
11
+ """A class to interact with the OpenSubtitles API."""
12
+
13
+ def __init__(self, credentials_path: str, **kwargs):
14
+ """Initialize the OpenSubtitles object with user credentials."""
15
+ self._users = self._parse_credentials(credentials_path)
16
+ if not self._users:
17
+ raise RuntimeError("No user credentials specified")
18
+ self._user_blacklist = set()
19
+ self._update_user_context(**kwargs)
20
+
21
+ def _parse_credentials(self, path: str):
22
+ """Parse the credentials from the given file path."""
23
+ with open(path) as fd:
24
+ credentials = json.load(fd)
25
+ return [
26
+ {
27
+ **{
28
+ "username": item["login"]["username"],
29
+ "password": item["login"]["password"]
30
+ },
31
+ **{
32
+ slugify(field["name"].lower(), separator="_", lowercase=True): field["value"]
33
+ for field in item["fields"]
34
+ }
35
+ }
36
+ for item in credentials.get("items", [])
37
+ ]
38
+
39
+ def _get_user_context(self, **kwargs):
40
+ """Get the user context that is not in the blacklist."""
41
+ ctx = next((ctx for ctx in self._users if ctx["username"] not in self._user_blacklist), None)
42
+ if not ctx:
43
+ raise RuntimeError("No user context available")
44
+ return ctx
45
+
46
+ def _update_user_context(self, **kwargs):
47
+ """Update the current user context."""
48
+ self._current_ctx = self._get_user_context(**kwargs)
49
+ payload = {
50
+ "username": self._current_ctx["username"],
51
+ "password": self._current_ctx["password"],
52
+ }
53
+ r = requests.post(
54
+ url="https://api.opensubtitles.com/api/v1/login",
55
+ headers={
56
+ 'Content-Type': "application/json",
57
+ 'Accept': "application/json",
58
+ 'Api-Key': self._current_ctx["api_key"],
59
+ 'User-Agent': self._current_ctx["user_agent"],
60
+ },
61
+ data=json.dumps(payload)
62
+ )
63
+ if r.ok:
64
+ response = r.json()
65
+ self._current_ctx["token"] = response.get("token")
66
+ else:
67
+ logging.error(r.text)
68
+ raise RuntimeError("Failed to update user context")
69
+
70
+ def get_subtitles(self, **kwargs):
71
+ """Get subtitles from the OpenSubtitles API."""
72
+ r = requests.get(
73
+ url=f"https://api.opensubtitles.com/api/v1/subtitles",
74
+ headers={
75
+ "Api-Key": self._current_ctx["api_key"],
76
+ "User-Agent": self._current_ctx["user_agent"],
77
+ },
78
+ params=kwargs,
79
+ )
80
+ if r.ok:
81
+ return r.json()
82
+ else:
83
+ raise RuntimeError(f"Failed to get subtitles [status={r.status_code}]")
84
+
85
+ def get_download_link(self, **kwargs):
86
+ """Get the download link for the given file ID."""
87
+ file_id = kwargs.get("file_id")
88
+ if not isinstance(file_id, int):
89
+ raise RuntimeError("file_id must be of type int")
90
+ r = requests.post(
91
+ url="https://api.opensubtitles.com/api/v1/download",
92
+ headers={
93
+ 'Accept': "application/json",
94
+ 'Api-Key': self._current_ctx["api_key"],
95
+ "Authorization": f"Bearer {self._current_ctx['token']}",
96
+ "User-Agent": self._current_ctx["user_agent"],
97
+ 'Content-Type': "application/json",
98
+ },
99
+ data=json.dumps({
100
+ "file_id": file_id
101
+ })
102
+ )
103
+ if r.ok:
104
+ response = r.json()
105
+ remaining = response["remaining"]
106
+ if remaining <= 0:
107
+ logging.info("Quota reached, updating user context")
108
+ self._user_blacklist.add(self._current_ctx["username"])
109
+ self._update_user_context(**kwargs)
110
+ logging.info("Trying again...")
111
+ return self.get_download_link(**kwargs)
112
+ else:
113
+ return response["link"]
114
+ else:
115
+ logging.error(f"Failed to get download link for file_id={file_id} status={r.status_code}")
116
+ raise RuntimeError(f"Failed to get download link for file_id={file_id} status={r.status_code}")
117
+
118
+
119
+ if __name__ == '__main__':
120
+ import argparse
121
+ parser = argparse.ArgumentParser(description='Download subtitles from OpenSubtitles')
122
+ parser.add_argument('--credentials', required=True, help='Path to credentials file')
123
+ args = parser.parse_args()
124
+
125
+ oss = OpenSubtitles(credentials_path=args.credentials)
126
+ data = oss.get_subtitles(imdb_id="1375666", languages=",".join(("nl", "en")))
127
+
128
+ for sub in data["data"]:
129
+ files = sub["attributes"]["files"]
130
+ for f in files:
131
+ fid = f["file_id"]
132
+ link = oss.get_download_link(file_id=fid)
133
+ for _ in range(100):
134
+ try:
135
+ r = requests.get(link)
136
+ if r.ok:
137
+ with open(f"/Users/david/Downloads/subs/{fid}.srt", "wb") as fd:
138
+ fd.write(r.content)
139
+ break
140
+ else:
141
+ raise Exception(f"Bad status: {r.status_code}")
142
+ except Exception as e:
143
+ logging.error(e)
144
+ time.sleep(0.1)