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.
- plexflow/__init__.py +0 -0
- plexflow/__main__.py +15 -0
- plexflow/core/.DS_Store +0 -0
- plexflow/core/__init__.py +0 -0
- plexflow/core/context/__init__.py +0 -0
- plexflow/core/context/metadata/__init__.py +0 -0
- plexflow/core/context/metadata/context.py +32 -0
- plexflow/core/context/metadata/tmdb/__init__.py +0 -0
- plexflow/core/context/metadata/tmdb/context.py +45 -0
- plexflow/core/context/partial_context.py +46 -0
- plexflow/core/context/partials/__init__.py +8 -0
- plexflow/core/context/partials/cache.py +16 -0
- plexflow/core/context/partials/context.py +12 -0
- plexflow/core/context/partials/ids.py +37 -0
- plexflow/core/context/partials/movie.py +115 -0
- plexflow/core/context/partials/tgx_batch.py +33 -0
- plexflow/core/context/partials/tgx_context.py +34 -0
- plexflow/core/context/partials/torrents.py +23 -0
- plexflow/core/context/partials/watchlist.py +35 -0
- plexflow/core/context/plexflow_context.py +29 -0
- plexflow/core/context/plexflow_property.py +36 -0
- plexflow/core/context/root/__init__.py +0 -0
- plexflow/core/context/root/context.py +25 -0
- plexflow/core/context/select/__init__.py +0 -0
- plexflow/core/context/select/context.py +45 -0
- plexflow/core/context/torrent/__init__.py +0 -0
- plexflow/core/context/torrent/context.py +43 -0
- plexflow/core/context/torrent/tpb/__init__.py +0 -0
- plexflow/core/context/torrent/tpb/context.py +45 -0
- plexflow/core/context/torrent/yts/__init__.py +0 -0
- plexflow/core/context/torrent/yts/context.py +45 -0
- plexflow/core/context/watchlist/__init__.py +0 -0
- plexflow/core/context/watchlist/context.py +46 -0
- plexflow/core/downloads/__init__.py +0 -0
- plexflow/core/downloads/candidates/__init__.py +0 -0
- plexflow/core/downloads/candidates/download_candidate.py +210 -0
- plexflow/core/downloads/candidates/filtered.py +51 -0
- plexflow/core/downloads/candidates/utils.py +39 -0
- plexflow/core/env/__init__.py +0 -0
- plexflow/core/env/env.py +31 -0
- plexflow/core/genai/__init__.py +0 -0
- plexflow/core/genai/bot.py +9 -0
- plexflow/core/genai/plexa.py +54 -0
- plexflow/core/genai/torrent/imdb_verify.py +65 -0
- plexflow/core/genai/torrent/movie.py +25 -0
- plexflow/core/genai/utils/__init__.py +0 -0
- plexflow/core/genai/utils/loader.py +5 -0
- plexflow/core/metadata/__init__.py +0 -0
- plexflow/core/metadata/auto/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_meta.py +40 -0
- plexflow/core/metadata/auto/auto_providers/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/auto/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/auto/episode.py +49 -0
- plexflow/core/metadata/auto/auto_providers/auto/item.py +55 -0
- plexflow/core/metadata/auto/auto_providers/auto/movie.py +13 -0
- plexflow/core/metadata/auto/auto_providers/auto/season.py +43 -0
- plexflow/core/metadata/auto/auto_providers/auto/show.py +26 -0
- plexflow/core/metadata/auto/auto_providers/imdb/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/imdb/movie.py +36 -0
- plexflow/core/metadata/auto/auto_providers/imdb/show.py +45 -0
- plexflow/core/metadata/auto/auto_providers/moviemeter/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/moviemeter/movie.py +40 -0
- plexflow/core/metadata/auto/auto_providers/plex/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/plex/movie.py +39 -0
- plexflow/core/metadata/auto/auto_providers/tmdb/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/tmdb/episode.py +30 -0
- plexflow/core/metadata/auto/auto_providers/tmdb/movie.py +36 -0
- plexflow/core/metadata/auto/auto_providers/tmdb/season.py +23 -0
- plexflow/core/metadata/auto/auto_providers/tmdb/show.py +41 -0
- plexflow/core/metadata/auto/auto_providers/tmdb.py +92 -0
- plexflow/core/metadata/auto/auto_providers/tvdb/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/tvdb/episode.py +28 -0
- plexflow/core/metadata/auto/auto_providers/tvdb/movie.py +36 -0
- plexflow/core/metadata/auto/auto_providers/tvdb/season.py +25 -0
- plexflow/core/metadata/auto/auto_providers/tvdb/show.py +41 -0
- plexflow/core/metadata/providers/__init__.py +0 -0
- plexflow/core/metadata/providers/imdb/__init__.py +0 -0
- plexflow/core/metadata/providers/imdb/datatypes.py +53 -0
- plexflow/core/metadata/providers/imdb/imdb.py +112 -0
- plexflow/core/metadata/providers/moviemeter/__init__.py +0 -0
- plexflow/core/metadata/providers/moviemeter/datatypes.py +111 -0
- plexflow/core/metadata/providers/moviemeter/moviemeter.py +42 -0
- plexflow/core/metadata/providers/plex/__init__.py +0 -0
- plexflow/core/metadata/providers/plex/datatypes.py +693 -0
- plexflow/core/metadata/providers/plex/plex.py +167 -0
- plexflow/core/metadata/providers/tmdb/__init__.py +0 -0
- plexflow/core/metadata/providers/tmdb/datatypes.py +460 -0
- plexflow/core/metadata/providers/tmdb/tmdb.py +85 -0
- plexflow/core/metadata/providers/tvdb/__init__.py +0 -0
- plexflow/core/metadata/providers/tvdb/datatypes.py +257 -0
- plexflow/core/metadata/providers/tvdb/tv_datatypes.py +554 -0
- plexflow/core/metadata/providers/tvdb/tvdb.py +65 -0
- plexflow/core/metadata/providers/universal/__init__.py +0 -0
- plexflow/core/metadata/providers/universal/movie.py +130 -0
- plexflow/core/metadata/providers/universal/old.py +192 -0
- plexflow/core/metadata/providers/universal/show.py +107 -0
- plexflow/core/plex/__init__.py +0 -0
- plexflow/core/plex/api/context/authorized.py +15 -0
- plexflow/core/plex/api/context/discover.py +14 -0
- plexflow/core/plex/api/context/library.py +14 -0
- plexflow/core/plex/discover/__init__.py +0 -0
- plexflow/core/plex/discover/activity.py +448 -0
- plexflow/core/plex/discover/comment.py +89 -0
- plexflow/core/plex/discover/feed.py +11 -0
- plexflow/core/plex/hooks/__init__.py +0 -0
- plexflow/core/plex/hooks/plex_authorized.py +60 -0
- plexflow/core/plex/hooks/plexflow_database.py +6 -0
- plexflow/core/plex/library/__init__.py +0 -0
- plexflow/core/plex/library/library.py +103 -0
- plexflow/core/plex/token/__init__.py +0 -0
- plexflow/core/plex/token/auto_token.py +91 -0
- plexflow/core/plex/utils/__init__.py +0 -0
- plexflow/core/plex/utils/paginated.py +39 -0
- plexflow/core/plex/watchlist/__init__.py +0 -0
- plexflow/core/plex/watchlist/datatypes.py +124 -0
- plexflow/core/plex/watchlist/watchlist.py +23 -0
- plexflow/core/storage/__init__.py +0 -0
- plexflow/core/storage/object/__init__.py +0 -0
- plexflow/core/storage/object/plexflow_storage.py +143 -0
- plexflow/core/storage/object/redis_storage.py +169 -0
- plexflow/core/subtitles/__init__.py +0 -0
- plexflow/core/subtitles/providers/__init__.py +0 -0
- plexflow/core/subtitles/providers/auto_subtitles.py +48 -0
- plexflow/core/subtitles/providers/oss/__init__.py +0 -0
- plexflow/core/subtitles/providers/oss/datatypes.py +104 -0
- plexflow/core/subtitles/providers/oss/download.py +48 -0
- plexflow/core/subtitles/providers/oss/old.py +144 -0
- plexflow/core/subtitles/providers/oss/oss.py +400 -0
- plexflow/core/subtitles/providers/oss/oss_subtitle.py +32 -0
- plexflow/core/subtitles/providers/oss/search.py +52 -0
- plexflow/core/subtitles/providers/oss/unlimited_oss.py +231 -0
- plexflow/core/subtitles/providers/oss/utils/__init__.py +0 -0
- plexflow/core/subtitles/providers/oss/utils/config.py +63 -0
- plexflow/core/subtitles/providers/oss/utils/download_client.py +22 -0
- plexflow/core/subtitles/providers/oss/utils/exceptions.py +35 -0
- plexflow/core/subtitles/providers/oss/utils/file_utils.py +83 -0
- plexflow/core/subtitles/providers/oss/utils/languages.py +78 -0
- plexflow/core/subtitles/providers/oss/utils/response_base.py +221 -0
- plexflow/core/subtitles/providers/oss/utils/responses.py +176 -0
- plexflow/core/subtitles/providers/oss/utils/srt.py +561 -0
- plexflow/core/subtitles/results/__init__.py +0 -0
- plexflow/core/subtitles/results/subtitle.py +170 -0
- plexflow/core/torrents/__init__.py +0 -0
- plexflow/core/torrents/analyzers/analyzed_torrent.py +143 -0
- plexflow/core/torrents/analyzers/analyzer.py +45 -0
- plexflow/core/torrents/analyzers/torrentquest/analyzer.py +47 -0
- plexflow/core/torrents/auto/auto_providers/auto/__init__.py +0 -0
- plexflow/core/torrents/auto/auto_providers/auto/torrent.py +64 -0
- plexflow/core/torrents/auto/auto_providers/tpb/torrent.py +62 -0
- plexflow/core/torrents/auto/auto_torrents.py +29 -0
- plexflow/core/torrents/providers/__init__.py +0 -0
- plexflow/core/torrents/providers/ext/__init__.py +0 -0
- plexflow/core/torrents/providers/ext/ext.py +18 -0
- plexflow/core/torrents/providers/ext/utils.py +64 -0
- plexflow/core/torrents/providers/extratorrent/__init__.py +0 -0
- plexflow/core/torrents/providers/extratorrent/extratorrent.py +21 -0
- plexflow/core/torrents/providers/extratorrent/utils.py +66 -0
- plexflow/core/torrents/providers/eztv/__init__.py +0 -0
- plexflow/core/torrents/providers/eztv/eztv.py +47 -0
- plexflow/core/torrents/providers/eztv/utils.py +83 -0
- plexflow/core/torrents/providers/rarbg2/__init__.py +0 -0
- plexflow/core/torrents/providers/rarbg2/rarbg2.py +19 -0
- plexflow/core/torrents/providers/rarbg2/utils.py +76 -0
- plexflow/core/torrents/providers/snowfl/__init__.py +0 -0
- plexflow/core/torrents/providers/snowfl/snowfl.py +36 -0
- plexflow/core/torrents/providers/snowfl/utils.py +59 -0
- plexflow/core/torrents/providers/tgx/__init__.py +0 -0
- plexflow/core/torrents/providers/tgx/context.py +50 -0
- plexflow/core/torrents/providers/tgx/dump.py +40 -0
- plexflow/core/torrents/providers/tgx/tgx.py +22 -0
- plexflow/core/torrents/providers/tgx/utils.py +61 -0
- plexflow/core/torrents/providers/therarbg/__init__.py +0 -0
- plexflow/core/torrents/providers/therarbg/therarbg.py +17 -0
- plexflow/core/torrents/providers/therarbg/utils.py +61 -0
- plexflow/core/torrents/providers/torrentquest/__init__.py +0 -0
- plexflow/core/torrents/providers/torrentquest/torrentquest.py +20 -0
- plexflow/core/torrents/providers/torrentquest/utils.py +70 -0
- plexflow/core/torrents/providers/tpb/__init__.py +0 -0
- plexflow/core/torrents/providers/tpb/tpb.py +17 -0
- plexflow/core/torrents/providers/tpb/utils.py +139 -0
- plexflow/core/torrents/providers/yts/__init__.py +0 -0
- plexflow/core/torrents/providers/yts/utils.py +57 -0
- plexflow/core/torrents/providers/yts/yts.py +31 -0
- plexflow/core/torrents/results/__init__.py +0 -0
- plexflow/core/torrents/results/torrent.py +165 -0
- plexflow/core/torrents/results/universal.py +220 -0
- plexflow/core/torrents/results/utils.py +15 -0
- plexflow/events/__init__.py +0 -0
- plexflow/events/download/__init__.py +0 -0
- plexflow/events/download/torrent_events.py +96 -0
- plexflow/events/publish/__init__.py +0 -0
- plexflow/events/publish/publish.py +34 -0
- plexflow/logging/__init__.py +0 -0
- plexflow/logging/log_setup.py +8 -0
- plexflow/spiders/quiet_logger.py +9 -0
- plexflow/spiders/tgx/pipelines/dump_json_pipeline.py +30 -0
- plexflow/spiders/tgx/pipelines/meta_pipeline.py +13 -0
- plexflow/spiders/tgx/pipelines/publish_pipeline.py +14 -0
- plexflow/spiders/tgx/pipelines/torrent_info_pipeline.py +12 -0
- plexflow/spiders/tgx/pipelines/validation_pipeline.py +17 -0
- plexflow/spiders/tgx/settings.py +36 -0
- plexflow/spiders/tgx/spider.py +72 -0
- plexflow/utils/__init__.py +0 -0
- plexflow/utils/antibot/human_like_requests.py +122 -0
- plexflow/utils/api/__init__.py +0 -0
- plexflow/utils/api/context/http.py +62 -0
- plexflow/utils/api/rest/__init__.py +0 -0
- plexflow/utils/api/rest/antibot_restful.py +68 -0
- plexflow/utils/api/rest/restful.py +49 -0
- plexflow/utils/captcha/__init__.py +0 -0
- plexflow/utils/captcha/bypass/__init__.py +0 -0
- plexflow/utils/captcha/bypass/decode_audio.py +34 -0
- plexflow/utils/download/__init__.py +0 -0
- plexflow/utils/download/gz.py +26 -0
- plexflow/utils/filesystem/__init__.py +0 -0
- plexflow/utils/filesystem/search.py +129 -0
- plexflow/utils/gmail/__init__.py +0 -0
- plexflow/utils/gmail/mails.py +116 -0
- plexflow/utils/hooks/__init__.py +0 -0
- plexflow/utils/hooks/http.py +84 -0
- plexflow/utils/hooks/postgresql.py +93 -0
- plexflow/utils/hooks/redis.py +112 -0
- plexflow/utils/image/storage.py +36 -0
- plexflow/utils/imdb/__init__.py +0 -0
- plexflow/utils/imdb/imdb_codes.py +107 -0
- plexflow/utils/pubsub/consume.py +82 -0
- plexflow/utils/pubsub/produce.py +25 -0
- plexflow/utils/retry/__init__.py +0 -0
- plexflow/utils/retry/utils.py +38 -0
- plexflow/utils/strings/__init__.py +0 -0
- plexflow/utils/strings/filesize.py +55 -0
- plexflow/utils/strings/language.py +14 -0
- plexflow/utils/subtitle/search.py +76 -0
- plexflow/utils/tasks/decorators.py +78 -0
- plexflow/utils/tasks/k8s/task.py +70 -0
- plexflow/utils/thread_safe/safe_list.py +54 -0
- plexflow/utils/thread_safe/safe_set.py +69 -0
- plexflow/utils/torrent/__init__.py +0 -0
- plexflow/utils/torrent/analyze.py +118 -0
- plexflow/utils/torrent/extract/common.py +37 -0
- plexflow/utils/torrent/extract/ext.py +2391 -0
- plexflow/utils/torrent/extract/extratorrent.py +56 -0
- plexflow/utils/torrent/extract/kat.py +1581 -0
- plexflow/utils/torrent/extract/tgx.py +96 -0
- plexflow/utils/torrent/extract/therarbg.py +170 -0
- plexflow/utils/torrent/extract/torrentquest.py +171 -0
- plexflow/utils/torrent/files.py +36 -0
- plexflow/utils/torrent/hash.py +90 -0
- plexflow/utils/transcribe/__init__.py +0 -0
- plexflow/utils/transcribe/speech2text.py +40 -0
- plexflow/utils/video/__init__.py +0 -0
- plexflow/utils/video/subtitle.py +73 -0
- plexflow-0.0.64.dist-info/METADATA +71 -0
- plexflow-0.0.64.dist-info/RECORD +256 -0
- plexflow-0.0.64.dist-info/WHEEL +4 -0
- 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)
|