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,170 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from datetime import datetime
|
3
|
+
from typing import List, Union
|
4
|
+
import PTN
|
5
|
+
|
6
|
+
class Subtitle(ABC):
|
7
|
+
"""
|
8
|
+
Abstract base class for subtitle objects.
|
9
|
+
"""
|
10
|
+
|
11
|
+
@property
|
12
|
+
@abstractmethod
|
13
|
+
def release_name(self) -> str:
|
14
|
+
"""
|
15
|
+
Gets the release name of the subtitle.
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
str: The release name of the subtitle.
|
19
|
+
"""
|
20
|
+
pass
|
21
|
+
|
22
|
+
@property
|
23
|
+
def uploader(self) -> str:
|
24
|
+
"""
|
25
|
+
Gets the uploader of the subtitle.
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
str: The uploader of the subtitle.
|
29
|
+
"""
|
30
|
+
return self.source
|
31
|
+
|
32
|
+
@property
|
33
|
+
@abstractmethod
|
34
|
+
def date(self) -> datetime:
|
35
|
+
"""
|
36
|
+
Gets the date of the subtitle.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
datetime: The date of the subtitle.
|
40
|
+
"""
|
41
|
+
pass
|
42
|
+
|
43
|
+
@property
|
44
|
+
@abstractmethod
|
45
|
+
def imdb_code(self) -> str:
|
46
|
+
"""
|
47
|
+
Gets the IMDb code of the subtitle.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
str: The IMDb code of the subtitle.
|
51
|
+
"""
|
52
|
+
pass
|
53
|
+
|
54
|
+
@property
|
55
|
+
def source(self) -> str:
|
56
|
+
"""
|
57
|
+
Gets the source of the subtitle.
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
str: The source of the subtitle.
|
61
|
+
"""
|
62
|
+
return self.src
|
63
|
+
|
64
|
+
@property
|
65
|
+
@abstractmethod
|
66
|
+
def language(self):
|
67
|
+
"""
|
68
|
+
Gets the language of the subtitle.
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
The language of the subtitle.
|
72
|
+
"""
|
73
|
+
pass
|
74
|
+
|
75
|
+
@property
|
76
|
+
def subtitle_id(self) -> str:
|
77
|
+
"""
|
78
|
+
Gets the subtitle ID.
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
str: The subtitle ID.
|
82
|
+
"""
|
83
|
+
return f"{self.source}_{self.hash}"
|
84
|
+
|
85
|
+
@property
|
86
|
+
def parsed_release_name(self) -> dict:
|
87
|
+
"""
|
88
|
+
Parses the release name of the subtitle.
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
dict: The parsed release name of the subtitle.
|
92
|
+
"""
|
93
|
+
return PTN.parse(self.release_name)
|
94
|
+
|
95
|
+
@property
|
96
|
+
def encoder_name(self) -> str:
|
97
|
+
"""
|
98
|
+
Gets the encoder name of the subtitle.
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
str: The encoder name of the subtitle.
|
102
|
+
"""
|
103
|
+
parts = self.parsed_release_name
|
104
|
+
return parts.get("encoder")
|
105
|
+
|
106
|
+
@property
|
107
|
+
def season(self) -> Union[int, List[int]]:
|
108
|
+
"""
|
109
|
+
Gets the season number of the subtitle.
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
Union[int, List[int]]: The season number of the subtitle.
|
113
|
+
"""
|
114
|
+
parts = self.parsed_release_name
|
115
|
+
return parts.get("season")
|
116
|
+
|
117
|
+
@property
|
118
|
+
def episode(self) -> Union[int, List[int]]:
|
119
|
+
"""
|
120
|
+
Gets the episode number of the subtitle.
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
Union[int, List[int]]: The episode number of the subtitle.
|
124
|
+
"""
|
125
|
+
parts = self.parsed_release_name
|
126
|
+
return parts.get("episode")
|
127
|
+
|
128
|
+
@property
|
129
|
+
def has_multiple_episodes(self):
|
130
|
+
"""
|
131
|
+
Checks if the subtitle has multiple episodes.
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
bool: True if the subtitle has multiple episodes, False otherwise.
|
135
|
+
"""
|
136
|
+
tmp = self.episode
|
137
|
+
return isinstance(tmp, list) and len(tmp) > 1
|
138
|
+
|
139
|
+
@property
|
140
|
+
def has_multiple_seasons(self):
|
141
|
+
"""
|
142
|
+
Checks if the subtitle has multiple seasons.
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
bool: True if the subtitle has multiple seasons, False otherwise.
|
146
|
+
"""
|
147
|
+
tmp = self.season
|
148
|
+
return isinstance(tmp, list) and len(tmp) > 1
|
149
|
+
|
150
|
+
@property
|
151
|
+
def title(self) -> str:
|
152
|
+
"""
|
153
|
+
Gets the title of the subtitle.
|
154
|
+
|
155
|
+
Returns:
|
156
|
+
str: The title of the subtitle.
|
157
|
+
"""
|
158
|
+
parts = self.parsed_release_name
|
159
|
+
return parts.get("title")
|
160
|
+
|
161
|
+
@property
|
162
|
+
def year(self) -> int:
|
163
|
+
"""
|
164
|
+
Gets the year of the subtitle.
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
int: The year of the subtitle.
|
168
|
+
"""
|
169
|
+
parts = self.parsed_release_name
|
170
|
+
return parts.get("year")
|
File without changes
|
@@ -0,0 +1,143 @@
|
|
1
|
+
from plexflow.core.torrents.results.torrent import Torrent
|
2
|
+
from datetime import datetime
|
3
|
+
from plexflow.utils.imdb.imdb_codes import IMDbCode
|
4
|
+
from typing import List, Union
|
5
|
+
from plexflow.utils.torrent.files import TorrentFile
|
6
|
+
|
7
|
+
class AnalyzedTorrent:
|
8
|
+
def __init__(self, torrent: Torrent, **kwargs):
|
9
|
+
self._torrent = torrent
|
10
|
+
self._details = kwargs
|
11
|
+
self._ai_details = {}
|
12
|
+
|
13
|
+
def update_from_ai(self, **kwargs):
|
14
|
+
self._ai_details.update(kwargs)
|
15
|
+
|
16
|
+
@property
|
17
|
+
def release_name(self):
|
18
|
+
return self._torrent.release_name or self._details.get('release_name') or self._ai_details.get('release_name')
|
19
|
+
|
20
|
+
@property
|
21
|
+
def seeds(self) -> int:
|
22
|
+
return self._torrent.seeds or self._details.get('seeds') or self._ai_details.get('seeds')
|
23
|
+
|
24
|
+
@property
|
25
|
+
def peers(self) -> int:
|
26
|
+
return self._torrent.peers or self._details.get('peers') or self._ai_details.get('peers')
|
27
|
+
|
28
|
+
@property
|
29
|
+
def size_bytes(self) -> int:
|
30
|
+
return self._torrent.size_bytes or self._details.get('size_bytes') or self._ai_details.get('size_bytes')
|
31
|
+
|
32
|
+
@property
|
33
|
+
def magnet(self) -> str:
|
34
|
+
return self._torrent.magnet or self._details.get('magnet')
|
35
|
+
|
36
|
+
@property
|
37
|
+
def hash(self) -> str:
|
38
|
+
return self._torrent.hash or self._details.get('hash')
|
39
|
+
|
40
|
+
@property
|
41
|
+
def uploader(self) -> str:
|
42
|
+
return self._torrent.uploader or self._details.get('uploader') or self._ai_details.get('uploader')
|
43
|
+
|
44
|
+
@property
|
45
|
+
def date(self) -> datetime:
|
46
|
+
return self._torrent.date or self._details.get('date') or self._ai_details.get('date')
|
47
|
+
|
48
|
+
@property
|
49
|
+
def imdb_code(self) -> IMDbCode:
|
50
|
+
return self._torrent.imdb_code or IMDbCode(self._details.get('imdb_code')) if self._details.get('imdb_code') else None
|
51
|
+
|
52
|
+
@property
|
53
|
+
def source(self) -> str:
|
54
|
+
return self._torrent.source
|
55
|
+
|
56
|
+
@property
|
57
|
+
def torrent_id(self) -> str:
|
58
|
+
return self._torrent.torrent_id or self._details.get('torrent_id')
|
59
|
+
|
60
|
+
@property
|
61
|
+
def parsed_release_name(self) -> dict:
|
62
|
+
return self._torrent.parsed_release_name
|
63
|
+
|
64
|
+
@property
|
65
|
+
def encoder_name(self) -> str:
|
66
|
+
return self._torrent.encoder_name
|
67
|
+
|
68
|
+
@property
|
69
|
+
def season(self) -> Union[int, List[int]]:
|
70
|
+
return self._torrent.season
|
71
|
+
|
72
|
+
@property
|
73
|
+
def episode(self) -> Union[int, List[int]]:
|
74
|
+
return self._torrent.episode
|
75
|
+
|
76
|
+
@property
|
77
|
+
def has_multiple_episodes(self):
|
78
|
+
return self._torrent.has_multiple_episodes
|
79
|
+
|
80
|
+
@property
|
81
|
+
def has_multiple_seasons(self):
|
82
|
+
return self._torrent.has_multiple_seasons
|
83
|
+
|
84
|
+
@property
|
85
|
+
def title(self) -> str:
|
86
|
+
return self._torrent.title
|
87
|
+
|
88
|
+
@property
|
89
|
+
def year(self) -> int:
|
90
|
+
return self._torrent.year
|
91
|
+
|
92
|
+
@property
|
93
|
+
def quality(self) -> str:
|
94
|
+
return self._torrent.quality
|
95
|
+
|
96
|
+
@property
|
97
|
+
def is_bad_quality(self):
|
98
|
+
return self._torrent.is_bad_quality
|
99
|
+
|
100
|
+
@property
|
101
|
+
def url(self) -> str:
|
102
|
+
return self._torrent.url
|
103
|
+
|
104
|
+
@property
|
105
|
+
def html_content(self) -> str:
|
106
|
+
return self._details.get('html_content')
|
107
|
+
|
108
|
+
@property
|
109
|
+
def resolved_urls(self) -> dict:
|
110
|
+
return self._details.get('resolved_urls')
|
111
|
+
|
112
|
+
@property
|
113
|
+
def files(self) -> List[TorrentFile]:
|
114
|
+
return self._details.get('file_list')
|
115
|
+
|
116
|
+
@property
|
117
|
+
def has_native_subtitles(self):
|
118
|
+
subs = self._details.get('subtitles')
|
119
|
+
if subs and len(subs) > 0:
|
120
|
+
return True
|
121
|
+
return False
|
122
|
+
|
123
|
+
@property
|
124
|
+
def has_native_dutch_subtitles(self):
|
125
|
+
subs = self._details.get('subtitles')
|
126
|
+
if subs and len(subs) > 0:
|
127
|
+
for sub in subs:
|
128
|
+
if 'dutch' in sub.lower():
|
129
|
+
return True
|
130
|
+
return False
|
131
|
+
|
132
|
+
@property
|
133
|
+
def has_native_english_subtitles(self):
|
134
|
+
subs = self._details.get('subtitles')
|
135
|
+
if subs and len(subs) > 0:
|
136
|
+
for sub in subs:
|
137
|
+
if 'english' in sub.lower():
|
138
|
+
return True
|
139
|
+
return False
|
140
|
+
|
141
|
+
@property
|
142
|
+
def native_subtitles(self):
|
143
|
+
return self._details.get('subtitles')
|
@@ -0,0 +1,45 @@
|
|
1
|
+
from plexflow.core.torrents.results.torrent import Torrent
|
2
|
+
import requests
|
3
|
+
from plexflow.core.torrents.analyzers.analyzed_torrent import AnalyzedTorrent
|
4
|
+
from abc import ABC, abstractmethod
|
5
|
+
|
6
|
+
class TorrentAnalyzer(ABC):
|
7
|
+
def __init__(self, torrent: Torrent):
|
8
|
+
self._torrent = torrent
|
9
|
+
|
10
|
+
self._resolved_urls = self.resolve_urls()
|
11
|
+
self._full_urls_content = '\n\n'.join(self._resolved_urls.values())
|
12
|
+
|
13
|
+
def resolve_urls(self):
|
14
|
+
resolved_urls = {}
|
15
|
+
|
16
|
+
if self._torrent.url:
|
17
|
+
print(f"Resolving URL: {self._torrent.url}")
|
18
|
+
r = requests.get(
|
19
|
+
url=self._torrent.url,
|
20
|
+
headers=self._headers,
|
21
|
+
)
|
22
|
+
|
23
|
+
r.raise_for_status()
|
24
|
+
|
25
|
+
html = r.text
|
26
|
+
|
27
|
+
resolved_urls[self._torrent.url] = html
|
28
|
+
else:
|
29
|
+
print(f"Torrent {self._torrent} does not have a URL.")
|
30
|
+
|
31
|
+
return resolved_urls
|
32
|
+
|
33
|
+
@abstractmethod
|
34
|
+
def do_analysis(self):
|
35
|
+
pass
|
36
|
+
|
37
|
+
def analyze(self):
|
38
|
+
details = self.do_analysis()
|
39
|
+
if isinstance(details, dict):
|
40
|
+
details = {
|
41
|
+
**details,
|
42
|
+
'resolved_urls': self._resolved_urls,
|
43
|
+
'html_content': self._full_urls_content,
|
44
|
+
}
|
45
|
+
return AnalyzedTorrent(self._torrent, **details)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
from plexflow.core.torrents.results.torrent import Torrent
|
2
|
+
from plexflow.utils.torrent.extract.torrentquest import extract_torrent_details
|
3
|
+
import requests
|
4
|
+
import re
|
5
|
+
from plexflow.core.torrents.analyzers.analyzed_torrent import AnalyzedTorrent
|
6
|
+
from plexflow.core.torrents.analyzers.analyzer import TorrentAnalyzer
|
7
|
+
|
8
|
+
class TorrentQuestAnalyzer(TorrentAnalyzer):
|
9
|
+
def __init__(self, torrent: Torrent):
|
10
|
+
self._nfo_link_pattern = re.compile(r"information\('file_([^']+)'\)", re.IGNORECASE)
|
11
|
+
|
12
|
+
self._headers= {
|
13
|
+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
14
|
+
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36'
|
15
|
+
}
|
16
|
+
|
17
|
+
super().__init__(torrent)
|
18
|
+
|
19
|
+
def resolve_urls(self):
|
20
|
+
resolved_urls = super().resolve_urls()
|
21
|
+
|
22
|
+
if len(resolved_urls) > 0:
|
23
|
+
html = next(iter(resolved_urls.values()))
|
24
|
+
nfo_match = next(re.finditer(self._nfo_link_pattern, html), None)
|
25
|
+
|
26
|
+
if nfo_match:
|
27
|
+
nfo_id = nfo_match.group(1)
|
28
|
+
nfo_url = f"https://torrentquest.com/info/{nfo_id}"
|
29
|
+
|
30
|
+
print(f"Resolving NFO URL: {nfo_url}")
|
31
|
+
r_nfo = requests.get(
|
32
|
+
url=nfo_url,
|
33
|
+
headers=self._headers,
|
34
|
+
)
|
35
|
+
|
36
|
+
r_nfo.raise_for_status()
|
37
|
+
|
38
|
+
nfo_html = r_nfo.text
|
39
|
+
|
40
|
+
resolved_urls[nfo_url] = nfo_html
|
41
|
+
|
42
|
+
return resolved_urls
|
43
|
+
|
44
|
+
def do_analysis(self):
|
45
|
+
# resolve torrent URL and extract the torrent information from the page
|
46
|
+
details = extract_torrent_details(self._full_urls_content)
|
47
|
+
return details
|
File without changes
|
@@ -0,0 +1,64 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import Any
|
3
|
+
from plexflow.utils.torrent.hash import extract_torrent_hash
|
4
|
+
import PTN
|
5
|
+
|
6
|
+
class AutoTorrent(ABC):
|
7
|
+
def __init__(self, imdb_id: str, release_name: str, magnet_uri: str, source: str) -> None:
|
8
|
+
self._imdb_id = imdb_id
|
9
|
+
self._release_name = release_name
|
10
|
+
self._magnet_uri = magnet_uri
|
11
|
+
self._source = source
|
12
|
+
|
13
|
+
@property
|
14
|
+
@abstractmethod
|
15
|
+
def id(self) -> Any:
|
16
|
+
pass
|
17
|
+
|
18
|
+
@property
|
19
|
+
def hash(self) -> str:
|
20
|
+
return extract_torrent_hash(self.magnet_uri)
|
21
|
+
|
22
|
+
@property
|
23
|
+
def release_name(self) -> str:
|
24
|
+
return self._release_name
|
25
|
+
|
26
|
+
@property
|
27
|
+
def magnet_uri(self) -> str:
|
28
|
+
return self._magnet_uri
|
29
|
+
|
30
|
+
@property
|
31
|
+
def parsed_release_name(self) -> dict:
|
32
|
+
return PTN.parse(self.release_name)
|
33
|
+
|
34
|
+
@property
|
35
|
+
def title(self) -> str:
|
36
|
+
return self.parsed_release_name.get("title")
|
37
|
+
|
38
|
+
@property
|
39
|
+
def year(self) -> int:
|
40
|
+
return self.parsed_release_name.get("year")
|
41
|
+
|
42
|
+
@property
|
43
|
+
def encoder(self) -> str:
|
44
|
+
return self.parsed_release_name.get("encoder")
|
45
|
+
|
46
|
+
@property
|
47
|
+
def resolution(self) -> str:
|
48
|
+
return self.parsed_release_name.get("resolution")
|
49
|
+
|
50
|
+
@property
|
51
|
+
def quality(self) -> str:
|
52
|
+
return self.parsed_release_name.get("quality")
|
53
|
+
|
54
|
+
@property
|
55
|
+
def is_cam(self) -> bool:
|
56
|
+
return self.parsed_release_name.get("quality") == "CAM"
|
57
|
+
|
58
|
+
@property
|
59
|
+
def imdb_id(self) -> str:
|
60
|
+
return self._imdb_id
|
61
|
+
|
62
|
+
@property
|
63
|
+
def source(self) -> str:
|
64
|
+
return self._source
|
@@ -0,0 +1,62 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import Any
|
3
|
+
from plexflow.utils.torrent.hash import extract_torrent_hash
|
4
|
+
import PTN
|
5
|
+
from plexflow.core.torrents.providers.tpb.utils import TPBSearchResult
|
6
|
+
|
7
|
+
class AutoTPBTorrent(ABC):
|
8
|
+
def __init__(self, torrent: TPBSearchResult) -> None:
|
9
|
+
super().__init__(imdb_id=torrent.imdb_id, release_name=torrent.title, magnet_uri=torrent.magnet_uri, source="TPB")
|
10
|
+
|
11
|
+
@property
|
12
|
+
@abstractmethod
|
13
|
+
def id(self) -> Any:
|
14
|
+
pass
|
15
|
+
|
16
|
+
@property
|
17
|
+
def hash(self) -> str:
|
18
|
+
return extract_torrent_hash(self.magnet_uri)
|
19
|
+
|
20
|
+
@property
|
21
|
+
def release_name(self) -> str:
|
22
|
+
return self._release_name
|
23
|
+
|
24
|
+
@property
|
25
|
+
def magnet_uri(self) -> str:
|
26
|
+
return self._magnet_uri
|
27
|
+
|
28
|
+
@property
|
29
|
+
def parsed_release_name(self) -> dict:
|
30
|
+
return PTN.parse(self.release_name)
|
31
|
+
|
32
|
+
@property
|
33
|
+
def title(self) -> str:
|
34
|
+
return self.parsed_release_name.get("title")
|
35
|
+
|
36
|
+
@property
|
37
|
+
def year(self) -> int:
|
38
|
+
return self.parsed_release_name.get("year")
|
39
|
+
|
40
|
+
@property
|
41
|
+
def encoder(self) -> str:
|
42
|
+
return self.parsed_release_name.get("encoder")
|
43
|
+
|
44
|
+
@property
|
45
|
+
def resolution(self) -> str:
|
46
|
+
return self.parsed_release_name.get("resolution")
|
47
|
+
|
48
|
+
@property
|
49
|
+
def quality(self) -> str:
|
50
|
+
return self.parsed_release_name.get("quality")
|
51
|
+
|
52
|
+
@property
|
53
|
+
def is_cam(self) -> bool:
|
54
|
+
return self.parsed_release_name.get("quality") == "CAM"
|
55
|
+
|
56
|
+
@property
|
57
|
+
def imdb_id(self) -> str:
|
58
|
+
return self._imdb_id
|
59
|
+
|
60
|
+
@property
|
61
|
+
def source(self) -> str:
|
62
|
+
return self._source
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from plexflow.core.torrents.providers.tpb.tpb import TPB
|
2
|
+
from plexflow.core.torrents.providers.yts.yts import YTS
|
3
|
+
from plexflow.core.torrents.providers.torrentquest.torrentquest import TorrentQuest
|
4
|
+
from plexflow.core.torrents.providers.extratorrent.extratorrent import ExtraTorrent
|
5
|
+
from plexflow.core.torrents.providers.ext.ext import Ext
|
6
|
+
from plexflow.core.torrents.providers.snowfl.snowfl import Snowfl
|
7
|
+
from plexflow.core.torrents.providers.therarbg.therarbg import TheRarbg
|
8
|
+
from typing import List
|
9
|
+
from plexflow.core.torrents.results.torrent import Torrent
|
10
|
+
|
11
|
+
class AutoTorrents:
|
12
|
+
@staticmethod
|
13
|
+
def movie(imdb_id: str = None, query: str = None, source: str = 'yts', **kwargs) -> List[Torrent]:
|
14
|
+
if source == 'tpb':
|
15
|
+
return TPB(**kwargs).search(query=imdb_id)
|
16
|
+
elif source == 'yts':
|
17
|
+
return YTS(**kwargs).search(query=imdb_id)
|
18
|
+
elif source == 'torrentquest':
|
19
|
+
return TorrentQuest(**kwargs).search(query=query)
|
20
|
+
elif source == 'extratorrent':
|
21
|
+
return ExtraTorrent(**kwargs).search(query=query)
|
22
|
+
elif source == 'therarbg':
|
23
|
+
return TheRarbg(**kwargs).search(query=imdb_id)
|
24
|
+
elif source == 'ext':
|
25
|
+
return Ext(**kwargs).search(query=query)
|
26
|
+
elif source == 'snowfl':
|
27
|
+
return Snowfl(**kwargs).search(query=query)
|
28
|
+
else:
|
29
|
+
raise ValueError(f"Invalid source: {source}")
|
File without changes
|
File without changes
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from plexflow.utils.api.rest.antibot_restful import AntibotRestful
|
2
|
+
from plexflow.core.torrents.providers.ext.utils import ExtSearchResult
|
3
|
+
from plexflow.utils.torrent.extract.ext import extract_torrent_results
|
4
|
+
from typing import List
|
5
|
+
import os
|
6
|
+
|
7
|
+
class Ext(AntibotRestful):
|
8
|
+
def __init__(self, base_url: str = 'https://ext.to', use_xvfb: bool = os.getenv('USE_XVFB', 'false').lower() == 'true'):
|
9
|
+
super().__init__(base_url=base_url, use_xvfb=use_xvfb)
|
10
|
+
|
11
|
+
def search(self, query: str) -> List[ExtSearchResult]:
|
12
|
+
capture = self.get('/search', query_params={
|
13
|
+
'c': 'movies',
|
14
|
+
'q': query
|
15
|
+
})
|
16
|
+
|
17
|
+
data = extract_torrent_results(html=capture.html)
|
18
|
+
return list(map(lambda t: ExtSearchResult(**t), data))
|
@@ -0,0 +1,64 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
from plexflow.core.torrents.results.torrent import Torrent
|
3
|
+
from plexflow.utils.imdb.imdb_codes import IMDbCode
|
4
|
+
from plexflow.utils.torrent.hash import extract_torrent_hash
|
5
|
+
|
6
|
+
class ExtSearchResult(Torrent):
|
7
|
+
def __init__(self, **kwargs):
|
8
|
+
super().__init__()
|
9
|
+
self._name = kwargs.get('name')
|
10
|
+
self._date = kwargs.get('date')
|
11
|
+
self._type = kwargs.get('type')
|
12
|
+
self._size = kwargs.get('size_bytes')
|
13
|
+
self._seeds = kwargs.get('seeds')
|
14
|
+
self._peers = kwargs.get('peers')
|
15
|
+
self._link = kwargs.get('link')
|
16
|
+
self.src = 'ext'
|
17
|
+
|
18
|
+
@property
|
19
|
+
def source(self) -> str:
|
20
|
+
return self.src
|
21
|
+
|
22
|
+
@property
|
23
|
+
def magnet(self) -> str:
|
24
|
+
return None
|
25
|
+
|
26
|
+
@property
|
27
|
+
def date(self) -> datetime:
|
28
|
+
return self._date
|
29
|
+
|
30
|
+
@property
|
31
|
+
def seeds(self) -> int:
|
32
|
+
return self._seeds
|
33
|
+
|
34
|
+
@property
|
35
|
+
def peers(self) -> int:
|
36
|
+
return self._peers
|
37
|
+
|
38
|
+
@property
|
39
|
+
def size_bytes(self) -> int:
|
40
|
+
return self._size
|
41
|
+
|
42
|
+
@property
|
43
|
+
def imdb_code(self) -> IMDbCode:
|
44
|
+
return None
|
45
|
+
|
46
|
+
@property
|
47
|
+
def release_name(self) -> str:
|
48
|
+
return self._name
|
49
|
+
|
50
|
+
@property
|
51
|
+
def hash(self) -> str:
|
52
|
+
return None
|
53
|
+
|
54
|
+
@property
|
55
|
+
def uploader(self) -> str:
|
56
|
+
return None
|
57
|
+
|
58
|
+
@property
|
59
|
+
def url(self) -> str:
|
60
|
+
return self._link
|
61
|
+
|
62
|
+
@property
|
63
|
+
def category(self) -> str:
|
64
|
+
return self._type
|
File without changes
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from plexflow.utils.api.rest.antibot_restful import AntibotRestful
|
2
|
+
from plexflow.core.torrents.providers.extratorrent.utils import ExtraTorrentSearchResult
|
3
|
+
from plexflow.utils.torrent.extract.extratorrent import extract_torrent_results
|
4
|
+
from typing import List
|
5
|
+
import os
|
6
|
+
|
7
|
+
class ExtraTorrent(AntibotRestful):
|
8
|
+
def __init__(self, base_url: str = 'https://extratorrent.st', use_xvfb: bool = os.getenv('USE_XVFB', 'false').lower() == 'true'):
|
9
|
+
super().__init__(base_url=base_url, use_xvfb=use_xvfb)
|
10
|
+
|
11
|
+
def search(self, query: str) -> List[ExtraTorrentSearchResult]:
|
12
|
+
capture = self.get('/search', query_params={
|
13
|
+
'search': query,
|
14
|
+
'new': 1,
|
15
|
+
'x': 0,
|
16
|
+
'y': 0,
|
17
|
+
's_cat': 1,
|
18
|
+
})
|
19
|
+
|
20
|
+
data = extract_torrent_results(html=capture.html)
|
21
|
+
return list(map(lambda t: ExtraTorrentSearchResult(**t), data))
|