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