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,103 @@
|
|
1
|
+
from plexflow.core.plex.api.context.library import PlexLibraryRequestContext
|
2
|
+
|
3
|
+
def get_library_id(library_name: str) -> int:
|
4
|
+
"""
|
5
|
+
Get the ID of a Plex library based on its name.
|
6
|
+
|
7
|
+
Parameters:
|
8
|
+
library_name (str): The name of the Plex library.
|
9
|
+
|
10
|
+
Returns:
|
11
|
+
int: The ID of the Plex library.
|
12
|
+
|
13
|
+
Raises:
|
14
|
+
ValueError: If the library name is not found.
|
15
|
+
|
16
|
+
Examples:
|
17
|
+
>>> get_library_id('Movies')
|
18
|
+
1
|
19
|
+
>>> get_library_id('TV Shows')
|
20
|
+
2
|
21
|
+
"""
|
22
|
+
context = PlexLibraryRequestContext()
|
23
|
+
response = context.get(f"/library/sections")
|
24
|
+
|
25
|
+
data = response.json()
|
26
|
+
sections = data["MediaContainer"]["Directory"]
|
27
|
+
for section in sections:
|
28
|
+
if section["title"] == library_name:
|
29
|
+
return section["key"]
|
30
|
+
raise ValueError(f"Library '{library_name}' not found.")
|
31
|
+
|
32
|
+
def is_media_in_library(guid: str, library_name: str, media_type: str) -> bool:
|
33
|
+
"""
|
34
|
+
Check if a media exists in a Plex library based on its GUID.
|
35
|
+
|
36
|
+
Parameters:
|
37
|
+
guid (str): The GUID of the media to search for.
|
38
|
+
library_name (str): The name of the Plex library.
|
39
|
+
media_type (str): The type of media (e.g., 'movie', 'show').
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
bool: True if the media is found in the library, False otherwise.
|
43
|
+
|
44
|
+
Examples:
|
45
|
+
>>> is_media_in_library('plex://movie/65581f1fb67b7b5555369f9c', 'Movies', 'movie')
|
46
|
+
True
|
47
|
+
>>> is_media_in_library('plex://show/65581f1fb67b7b5555369f9c', 'TV Shows', 'show')
|
48
|
+
False
|
49
|
+
"""
|
50
|
+
library_id = get_library_id(library_name)
|
51
|
+
context = PlexLibraryRequestContext()
|
52
|
+
response = context.get(f"/library/sections/{library_id}/all", params={
|
53
|
+
"guid": guid
|
54
|
+
})
|
55
|
+
|
56
|
+
data = response.json()
|
57
|
+
n_results = data["MediaContainer"]["size"]
|
58
|
+
|
59
|
+
if media_type == 'movie':
|
60
|
+
return n_results > 0
|
61
|
+
elif media_type == 'show':
|
62
|
+
return n_results > 0
|
63
|
+
else:
|
64
|
+
raise ValueError(f"Invalid media type: {media_type}")
|
65
|
+
|
66
|
+
def is_movie_in_library(guid: str, library_name: str = 'Films') -> bool:
|
67
|
+
"""
|
68
|
+
Check if a movie exists in a Plex library based on its GUID.
|
69
|
+
|
70
|
+
Parameters:
|
71
|
+
guid (str): The GUID of the movie to search for.
|
72
|
+
library_name (str): The name of the Plex library. Default is 'Films'.
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
bool: True if the movie is found in the library, False otherwise.
|
76
|
+
|
77
|
+
Examples:
|
78
|
+
>>> is_movie_in_library('plex://movie/65581f1fb67b7b5555369f9c', 'Movies')
|
79
|
+
True
|
80
|
+
>>> is_movie_in_library('plex://show/65581f1fb67b7b5555369f9c', 'TV Shows')
|
81
|
+
False
|
82
|
+
"""
|
83
|
+
return is_media_in_library(guid, library_name, 'movie')
|
84
|
+
|
85
|
+
def is_show_in_library(guid: str, library_name: str = 'Series') -> bool:
|
86
|
+
"""
|
87
|
+
Check if a TV show exists in a Plex library based on its GUID.
|
88
|
+
|
89
|
+
Parameters:
|
90
|
+
guid (str): The GUID of the TV show to search for.
|
91
|
+
library_name (str): The name of the Plex library. Default is 'Series'.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
bool: True if the TV show is found in the library, False otherwise.
|
95
|
+
|
96
|
+
Examples:
|
97
|
+
>>> is_show_in_library('plex://movie/65581f1fb67b7b5555369f9c', 'Movies')
|
98
|
+
True
|
99
|
+
>>> is_show_in_library('plex://show/65581f1fb67b7b5555369f9c', 'TV Shows')
|
100
|
+
False
|
101
|
+
True
|
102
|
+
"""
|
103
|
+
return is_media_in_library(guid, library_name, 'show')
|
File without changes
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import os
|
2
|
+
import redis
|
3
|
+
import xml.etree.ElementTree as ET
|
4
|
+
from typing import Union
|
5
|
+
|
6
|
+
class PlexAutoToken:
|
7
|
+
"""
|
8
|
+
A class to automatically try to fetch the value for PLEX_TOKEN from a number of locations.
|
9
|
+
|
10
|
+
This class tries to fetch the Plex token from the following sources in order:
|
11
|
+
1. Redis database (optional)
|
12
|
+
2. Environment variable 'PLEX_TOKEN'
|
13
|
+
3. A Plex configured Preferences.xml file specified by the user
|
14
|
+
4. A manually passed token
|
15
|
+
|
16
|
+
If the token is not found in any of these sources, a ValueError is raised.
|
17
|
+
|
18
|
+
Example:
|
19
|
+
```
|
20
|
+
plex_token = PlexAutoToken(redis_host='localhost', port=6379, db=0, file_path='/path/to/Preferences.xml').get_token()
|
21
|
+
print(plex_token) # Prints the fetched Plex token
|
22
|
+
```
|
23
|
+
|
24
|
+
Args:
|
25
|
+
redis_instance (Union[redis.Redis, None], optional): An instance of a Redis database. Defaults to None.
|
26
|
+
redis_host (str, optional): The host of the Redis database. Defaults to 'localhost'.
|
27
|
+
port (int, optional): The port of the Redis database. Defaults to 6379.
|
28
|
+
db (int, optional): The database index of the Redis database. Defaults to 0.
|
29
|
+
file_path (str, optional): The path to the Plex configured Preferences.xml file. Defaults to None.
|
30
|
+
plex_token (str, optional): A manually passed Plex token. Defaults to None.
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(self, redis_instance: Union[redis.Redis, None] = None, redis_host: str = 'localhost', port: int = 6379, db: int = 0, file_path: str = None, plex_token: str = None):
|
34
|
+
self.redis_instance = None
|
35
|
+
if redis_instance is not None:
|
36
|
+
self.redis_instance = redis_instance
|
37
|
+
elif redis_host is not None:
|
38
|
+
try:
|
39
|
+
self.redis_instance = redis.Redis(host=redis_host, port=port, db=db)
|
40
|
+
except redis.ConnectionError:
|
41
|
+
print("Unable to connect to Redis at {}:{}. Continuing without Redis.".format(redis_host, port))
|
42
|
+
self.file_path = file_path
|
43
|
+
self.plex_token = plex_token
|
44
|
+
|
45
|
+
def get_token(self) -> str:
|
46
|
+
"""
|
47
|
+
Fetches the Plex token from the sources specified in the class docstring.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
str: The fetched Plex token.
|
51
|
+
|
52
|
+
Raises:
|
53
|
+
ValueError: If the Plex token is not found in any of the sources.
|
54
|
+
|
55
|
+
Example:
|
56
|
+
```
|
57
|
+
plex_token = PlexAutoToken(redis_host='localhost', port=6379, db=0, file_path='/path/to/Preferences.xml').get_token()
|
58
|
+
print(plex_token) # Prints the fetched Plex token
|
59
|
+
```
|
60
|
+
"""
|
61
|
+
# Try to fetch the token from the Redis instance
|
62
|
+
try:
|
63
|
+
if self.redis_instance is not None:
|
64
|
+
token = self.redis_instance.get('PLEX_TOKEN')
|
65
|
+
if token is not None:
|
66
|
+
return token.decode()
|
67
|
+
except redis.RedisError as e:
|
68
|
+
pass
|
69
|
+
|
70
|
+
# Try to fetch the token from the environment variables
|
71
|
+
token = os.getenv('PLEX_TOKEN')
|
72
|
+
if token is not None:
|
73
|
+
return token
|
74
|
+
|
75
|
+
# Try to fetch the token from the Plex configured Preferences.xml file
|
76
|
+
if self.file_path is not None:
|
77
|
+
try:
|
78
|
+
tree = ET.parse(self.file_path)
|
79
|
+
root = tree.getroot()
|
80
|
+
token = root.attrib.get('PlexOnlineToken')
|
81
|
+
if token:
|
82
|
+
return token
|
83
|
+
except FileNotFoundError:
|
84
|
+
pass
|
85
|
+
|
86
|
+
# Use the manually passed token
|
87
|
+
if self.plex_token is not None:
|
88
|
+
return self.plex_token
|
89
|
+
|
90
|
+
# If the token is not found, raise an error
|
91
|
+
raise ValueError('PLEX_TOKEN not found')
|
File without changes
|
@@ -0,0 +1,39 @@
|
|
1
|
+
from typing import Iterator
|
2
|
+
|
3
|
+
def paginated(func) -> Iterator:
|
4
|
+
"""
|
5
|
+
Decorator function that enables paginated retrieval of data from the Plex API.
|
6
|
+
|
7
|
+
Args:
|
8
|
+
func: The function to be decorated.
|
9
|
+
|
10
|
+
Yields:
|
11
|
+
The partial response obtained from the decorated function.
|
12
|
+
|
13
|
+
Raises:
|
14
|
+
Exception: If an error occurs during the retrieval process.
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
The wrapper function that enables paginated retrieval.
|
18
|
+
"""
|
19
|
+
def wrapper():
|
20
|
+
container_start = 0
|
21
|
+
container_size = 100
|
22
|
+
container_end_reached = False
|
23
|
+
|
24
|
+
while not container_end_reached:
|
25
|
+
try:
|
26
|
+
partial_response = func(params={
|
27
|
+
"X-Plex-Container-Start": container_start,
|
28
|
+
"X-Plex-Container-Size": container_size,
|
29
|
+
})
|
30
|
+
|
31
|
+
yield partial_response
|
32
|
+
|
33
|
+
if len(partial_response.Metadata) == 0:
|
34
|
+
container_end_reached = True
|
35
|
+
|
36
|
+
container_start += container_size
|
37
|
+
except Exception as _:
|
38
|
+
container_end_reached = True
|
39
|
+
return wrapper
|
File without changes
|
@@ -0,0 +1,124 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from typing import Optional, List
|
3
|
+
import json
|
4
|
+
from dataclasses_json import dataclass_json, Undefined, CatchAll
|
5
|
+
|
6
|
+
@dataclass_json(undefined=Undefined.INCLUDE)
|
7
|
+
@dataclass
|
8
|
+
class PlexImage:
|
9
|
+
"""
|
10
|
+
Represents an image with optional alt text, type, and URL.
|
11
|
+
|
12
|
+
Attributes:
|
13
|
+
alt (Optional[str]): The alt text of the image.
|
14
|
+
type (Optional[str]): The type of the image.
|
15
|
+
url (Optional[str]): The URL of the image.
|
16
|
+
"""
|
17
|
+
alt: Optional[str] = field(default=None)
|
18
|
+
type: Optional[str] = field(default=None)
|
19
|
+
url: Optional[str] = field(default=None)
|
20
|
+
_catch_all: CatchAll = field(default_factory=dict)
|
21
|
+
|
22
|
+
@dataclass_json(undefined=Undefined.INCLUDE)
|
23
|
+
@dataclass
|
24
|
+
class PlexMetadata:
|
25
|
+
"""
|
26
|
+
Represents the metadata of a media item.
|
27
|
+
|
28
|
+
Attributes:
|
29
|
+
Image (Optional[List[Image]]): A list of Image objects associated with the media item.
|
30
|
+
addedAt (Optional[int]): The timestamp when the media item was added.
|
31
|
+
art (Optional[str]): The URL of the artwork of the media item.
|
32
|
+
audienceRating (Optional[float]): The audience rating of the media item.
|
33
|
+
audienceRatingImage (Optional[str]): The URL of the audience rating image.
|
34
|
+
banner (Optional[str]): The URL of the banner image.
|
35
|
+
contentRating (Optional[str]): The content rating of the media item.
|
36
|
+
duration (Optional[int]): The duration of the media item in seconds.
|
37
|
+
guid (Optional[str]): The globally unique identifier of the media item.
|
38
|
+
imdbRatingCount (Optional[int]): The number of IMDb ratings.
|
39
|
+
key (Optional[str]): The key of the media item.
|
40
|
+
originallyAvailableAt (Optional[str]): The original release date of the media item.
|
41
|
+
publicPagesURL (Optional[str]): The URL of the public pages.
|
42
|
+
rating (Optional[float]): The rating of the media item.
|
43
|
+
ratingImage (Optional[str]): The URL of the rating image.
|
44
|
+
ratingKey (Optional[str]): The rating key of the media item.
|
45
|
+
slug (Optional[str]): The slug of the media item.
|
46
|
+
studio (Optional[str]): The studio that produced the media item.
|
47
|
+
tagline (Optional[str]): The tagline of the media item.
|
48
|
+
thumb (Optional[str]): The URL of the thumbnail image.
|
49
|
+
title (Optional[str]): The title of the media item.
|
50
|
+
type (Optional[str]): The type of the media item.
|
51
|
+
userState (Optional[bool]): The user state of the media item.
|
52
|
+
year (Optional[int]): The release year of the media item.
|
53
|
+
"""
|
54
|
+
|
55
|
+
Image: Optional[List[PlexImage]] = field(default_factory=list)
|
56
|
+
addedAt: Optional[int] = None
|
57
|
+
art: Optional[str] = None
|
58
|
+
audienceRating: Optional[float] = None
|
59
|
+
audienceRatingImage: Optional[str] = None
|
60
|
+
banner: Optional[str] = None
|
61
|
+
contentRating: Optional[str] = None
|
62
|
+
duration: Optional[int] = None
|
63
|
+
guid: Optional[str] = None
|
64
|
+
imdbRatingCount: Optional[int] = None
|
65
|
+
key: Optional[str] = None
|
66
|
+
originallyAvailableAt: Optional[str] = None
|
67
|
+
publicPagesURL: Optional[str] = None
|
68
|
+
rating: Optional[float] = None
|
69
|
+
ratingImage: Optional[str] = None
|
70
|
+
ratingKey: Optional[str] = None
|
71
|
+
slug: Optional[str] = None
|
72
|
+
studio: Optional[str] = None
|
73
|
+
tagline: Optional[str] = None
|
74
|
+
thumb: Optional[str] = None
|
75
|
+
title: Optional[str] = None
|
76
|
+
type: Optional[str] = None
|
77
|
+
userState: Optional[bool] = None
|
78
|
+
year: Optional[int] = None
|
79
|
+
_catch_all: CatchAll = field(default_factory=dict)
|
80
|
+
|
81
|
+
def __post_init__(self):
|
82
|
+
self.Image = [PlexImage(**img) if isinstance(img, dict) else img for img in self.Image]
|
83
|
+
|
84
|
+
@property
|
85
|
+
def is_movie(self):
|
86
|
+
return self.type == "movie"
|
87
|
+
|
88
|
+
@property
|
89
|
+
def is_show(self):
|
90
|
+
return self.type == "show"
|
91
|
+
|
92
|
+
@dataclass_json(undefined=Undefined.INCLUDE)
|
93
|
+
@dataclass
|
94
|
+
class MediaContainer:
|
95
|
+
"""
|
96
|
+
Represents a media container with metadata and other properties.
|
97
|
+
|
98
|
+
Attributes:
|
99
|
+
Metadata (Optional[List[PlexMetadata]]): A list of Metadata objects associated with the media container.
|
100
|
+
identifier (Optional[str]): The identifier of the media container.
|
101
|
+
librarySectionID (Optional[str]): The ID of the library section.
|
102
|
+
librarySectionTitle (Optional[str]): The title of the library section.
|
103
|
+
offset (Optional[int]): The offset of the media container.
|
104
|
+
size (Optional[int]): The size of the media container.
|
105
|
+
totalSize (Optional[int]): The total size of the media container.
|
106
|
+
"""
|
107
|
+
|
108
|
+
Metadata: Optional[List[PlexMetadata]] = field(default_factory=list)
|
109
|
+
identifier: Optional[str] = None
|
110
|
+
librarySectionID: Optional[str] = None
|
111
|
+
librarySectionTitle: Optional[str] = None
|
112
|
+
offset: Optional[int] = None
|
113
|
+
size: Optional[int] = None
|
114
|
+
totalSize: Optional[int] = None
|
115
|
+
_catch_all: CatchAll = field(default_factory=dict)
|
116
|
+
|
117
|
+
def __post_init__(self):
|
118
|
+
self.Metadata = [PlexMetadata(**meta) if isinstance(meta, dict) else meta for meta in self.Metadata]
|
119
|
+
|
120
|
+
def from_json(json_str: str) -> MediaContainer:
|
121
|
+
try:
|
122
|
+
return MediaContainer(**json.loads(json_str).get("MediaContainer"))
|
123
|
+
except json.JSONDecodeError as e:
|
124
|
+
raise f"Error decoding JSON: {e}"
|
@@ -0,0 +1,23 @@
|
|
1
|
+
from plexflow.core.plex.hooks.plex_authorized import PlexAuthorizedHttpHook
|
2
|
+
from plexflow.core.plex.watchlist.datatypes import from_json, MediaContainer
|
3
|
+
from plexflow.core.plex.utils.paginated import paginated
|
4
|
+
from plexflow.core.plex.api.context.authorized import PlexAuthorizedRequestContext
|
5
|
+
|
6
|
+
@paginated
|
7
|
+
def get_watchlist(**kwargs) -> MediaContainer:
|
8
|
+
"""
|
9
|
+
Retrieves the watchlist from the Plex server.
|
10
|
+
|
11
|
+
Args:
|
12
|
+
**kwargs: Additional keyword arguments to be passed to the PlexAuthorizedHttpHook.
|
13
|
+
|
14
|
+
Returns:
|
15
|
+
MediaContainer: The watchlist as a MediaContainer object.
|
16
|
+
|
17
|
+
Raises:
|
18
|
+
None
|
19
|
+
|
20
|
+
"""
|
21
|
+
context = PlexAuthorizedRequestContext(base_url="https://metadata.provider.plex.tv")
|
22
|
+
response = context.get(endpoint="/library/sections/watchlist/all", **kwargs)
|
23
|
+
return from_json(response.content.decode("utf-8"))
|
File without changes
|
File without changes
|
@@ -0,0 +1,143 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from plexflow.core.storage.object.redis_storage import RedisObjectStore
|
3
|
+
from typing import Union
|
4
|
+
import os
|
5
|
+
|
6
|
+
class PlexflowObjectStore:
|
7
|
+
"""A class used to represent the Plexflow Object Store.
|
8
|
+
|
9
|
+
Attributes:
|
10
|
+
run_id (str): The run id of the object store.
|
11
|
+
host (str): The host of the Redis server. Defaults to 'localhost'.
|
12
|
+
port (int): The port of the Redis server. Defaults to 6379.
|
13
|
+
db (int): The database index of the Redis server. Defaults to 0.
|
14
|
+
default_ttl (int): The default TTL (in seconds) for the stored objects. Defaults to 3600.
|
15
|
+
root_key (Union[str, Path]): The root key in the object store. Defaults to "@plexflow".
|
16
|
+
bucket_store (RedisObjectStore): The RedisObjectStore instance.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(self, run_id: str, host: str = os.getenv('REDIS_HOST', 'localhost'), port: int = int(os.getenv('REDIS_PORT', 6379)), db: int = 0, default_ttl: int = 3600, root_key: Union[str, Path] = "@plexflow", password=os.getenv('REDIS_PASSWORD', None)):
|
20
|
+
"""Initializes the PlexflowObjectStore with the given parameters.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
run_id (str): The run id of the object store.
|
24
|
+
host (str, optional): The host of the Redis server. Defaults to 'localhost'.
|
25
|
+
port (int, optional): The port of the Redis server. Defaults to 6379.
|
26
|
+
db (int, optional): The database index of the Redis server. Defaults to 0.
|
27
|
+
default_ttl (int, optional): The default TTL (in seconds) for the stored objects. Defaults to 3600.
|
28
|
+
root_key (Union[str, Path], optional): The root key in the object store. Defaults to "@plexflow".
|
29
|
+
"""
|
30
|
+
|
31
|
+
self.bucket_store = RedisObjectStore(host=host, port=port, db=db, default_ttl=default_ttl, password=password)
|
32
|
+
self.root_key = Path(root_key) if isinstance(root_key, str) else root_key
|
33
|
+
self.run_id = run_id
|
34
|
+
|
35
|
+
@property
|
36
|
+
def bucket(self):
|
37
|
+
"""Gets the RedisObjectStore instance.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
RedisObjectStore: The RedisObjectStore instance.
|
41
|
+
"""
|
42
|
+
|
43
|
+
return self.bucket_store
|
44
|
+
|
45
|
+
def make_key(self, name: Union[str, Path]) -> Path:
|
46
|
+
"""Constructs a global key by joining root_key and name.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
name (Union[str, Path]): The name to be joined with root_key.
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
Path: The constructed global key.
|
53
|
+
"""
|
54
|
+
|
55
|
+
if isinstance(name, str):
|
56
|
+
name = Path(name)
|
57
|
+
return self.root_key / name
|
58
|
+
|
59
|
+
def make_run_key(self, name: Union[str, Path]) -> Path:
|
60
|
+
"""Constructs a run-specific key by joining root_key, run_id, and name.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
name (Union[str, Path]): The name to be joined with root_key and run_id.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
Path: The constructed run-specific key.
|
67
|
+
"""
|
68
|
+
|
69
|
+
return self.root_key / self.run_id / name
|
70
|
+
|
71
|
+
def store(self, key: Union[str, Path], obj, use_json=False):
|
72
|
+
"""Stores a serialized version of an object in the Redis store permanently.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
key (Union[str, Path]): The key under which the object is stored.
|
76
|
+
obj (Any): The object to be stored.
|
77
|
+
use_json (bool, optional): Whether to use JSON for serialization. Defaults to False.
|
78
|
+
"""
|
79
|
+
|
80
|
+
if isinstance(key, Path):
|
81
|
+
key = str(key)
|
82
|
+
self.bucket_store.store(key, obj, use_json)
|
83
|
+
|
84
|
+
def store_temporarily(self, key: Union[str, Path], obj, ttl=None, use_json=False):
|
85
|
+
"""Stores a serialized version of an object in the Redis store temporarily.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
key (Union[str, Path]): The key under which the object is stored.
|
89
|
+
obj (Any): The object to be stored.
|
90
|
+
ttl (int, optional): The TTL (in seconds) for the stored object. If not provided, default_ttl is used.
|
91
|
+
use_json (bool, optional): Whether to use JSON for serialization. Defaults to False.
|
92
|
+
"""
|
93
|
+
|
94
|
+
if isinstance(key, Path):
|
95
|
+
key = str(key)
|
96
|
+
self.bucket_store.store_temporarily(key, obj, ttl, use_json)
|
97
|
+
|
98
|
+
def retrieve(self, key: Union[str, Path], use_json=False):
|
99
|
+
"""Retrieves an object from the Redis store and deserializes it.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
key (Union[str, Path]): The key under which the object is stored.
|
103
|
+
use_json (bool, optional): Whether to use JSON for deserialization. Defaults to False.
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
Any: The deserialized object.
|
107
|
+
"""
|
108
|
+
|
109
|
+
if isinstance(key, Path):
|
110
|
+
key = str(key)
|
111
|
+
return self.bucket_store.retrieve(key, use_json)
|
112
|
+
|
113
|
+
def retrieve_keys(self, pattern):
|
114
|
+
"""Retrieves all keys matching a given pattern from the Redis store.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
pattern (str): The pattern to match.
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
list: A list of keys matching the pattern.
|
121
|
+
|
122
|
+
Raises:
|
123
|
+
Exception: If an error occurs during retrieving the keys.
|
124
|
+
|
125
|
+
"""
|
126
|
+
|
127
|
+
return self.bucket_store.retrieve_keys(pattern)
|
128
|
+
|
129
|
+
def retrieve_values(self, pattern):
|
130
|
+
"""Retrieves all values matching a given pattern from the Redis store.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
pattern (str): The pattern to match.
|
134
|
+
|
135
|
+
Returns:
|
136
|
+
list: A list of values matching the pattern.
|
137
|
+
|
138
|
+
Raises:
|
139
|
+
Exception: If an error occurs during retrieving the values.
|
140
|
+
|
141
|
+
"""
|
142
|
+
|
143
|
+
return self.bucket_store.retrieve_values(pattern)
|