plexflow 0.0.64__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. plexflow/__init__.py +0 -0
  2. plexflow/__main__.py +15 -0
  3. plexflow/core/.DS_Store +0 -0
  4. plexflow/core/__init__.py +0 -0
  5. plexflow/core/context/__init__.py +0 -0
  6. plexflow/core/context/metadata/__init__.py +0 -0
  7. plexflow/core/context/metadata/context.py +32 -0
  8. plexflow/core/context/metadata/tmdb/__init__.py +0 -0
  9. plexflow/core/context/metadata/tmdb/context.py +45 -0
  10. plexflow/core/context/partial_context.py +46 -0
  11. plexflow/core/context/partials/__init__.py +8 -0
  12. plexflow/core/context/partials/cache.py +16 -0
  13. plexflow/core/context/partials/context.py +12 -0
  14. plexflow/core/context/partials/ids.py +37 -0
  15. plexflow/core/context/partials/movie.py +115 -0
  16. plexflow/core/context/partials/tgx_batch.py +33 -0
  17. plexflow/core/context/partials/tgx_context.py +34 -0
  18. plexflow/core/context/partials/torrents.py +23 -0
  19. plexflow/core/context/partials/watchlist.py +35 -0
  20. plexflow/core/context/plexflow_context.py +29 -0
  21. plexflow/core/context/plexflow_property.py +36 -0
  22. plexflow/core/context/root/__init__.py +0 -0
  23. plexflow/core/context/root/context.py +25 -0
  24. plexflow/core/context/select/__init__.py +0 -0
  25. plexflow/core/context/select/context.py +45 -0
  26. plexflow/core/context/torrent/__init__.py +0 -0
  27. plexflow/core/context/torrent/context.py +43 -0
  28. plexflow/core/context/torrent/tpb/__init__.py +0 -0
  29. plexflow/core/context/torrent/tpb/context.py +45 -0
  30. plexflow/core/context/torrent/yts/__init__.py +0 -0
  31. plexflow/core/context/torrent/yts/context.py +45 -0
  32. plexflow/core/context/watchlist/__init__.py +0 -0
  33. plexflow/core/context/watchlist/context.py +46 -0
  34. plexflow/core/downloads/__init__.py +0 -0
  35. plexflow/core/downloads/candidates/__init__.py +0 -0
  36. plexflow/core/downloads/candidates/download_candidate.py +210 -0
  37. plexflow/core/downloads/candidates/filtered.py +51 -0
  38. plexflow/core/downloads/candidates/utils.py +39 -0
  39. plexflow/core/env/__init__.py +0 -0
  40. plexflow/core/env/env.py +31 -0
  41. plexflow/core/genai/__init__.py +0 -0
  42. plexflow/core/genai/bot.py +9 -0
  43. plexflow/core/genai/plexa.py +54 -0
  44. plexflow/core/genai/torrent/imdb_verify.py +65 -0
  45. plexflow/core/genai/torrent/movie.py +25 -0
  46. plexflow/core/genai/utils/__init__.py +0 -0
  47. plexflow/core/genai/utils/loader.py +5 -0
  48. plexflow/core/metadata/__init__.py +0 -0
  49. plexflow/core/metadata/auto/__init__.py +0 -0
  50. plexflow/core/metadata/auto/auto_meta.py +40 -0
  51. plexflow/core/metadata/auto/auto_providers/__init__.py +0 -0
  52. plexflow/core/metadata/auto/auto_providers/auto/__init__.py +0 -0
  53. plexflow/core/metadata/auto/auto_providers/auto/episode.py +49 -0
  54. plexflow/core/metadata/auto/auto_providers/auto/item.py +55 -0
  55. plexflow/core/metadata/auto/auto_providers/auto/movie.py +13 -0
  56. plexflow/core/metadata/auto/auto_providers/auto/season.py +43 -0
  57. plexflow/core/metadata/auto/auto_providers/auto/show.py +26 -0
  58. plexflow/core/metadata/auto/auto_providers/imdb/__init__.py +0 -0
  59. plexflow/core/metadata/auto/auto_providers/imdb/movie.py +36 -0
  60. plexflow/core/metadata/auto/auto_providers/imdb/show.py +45 -0
  61. plexflow/core/metadata/auto/auto_providers/moviemeter/__init__.py +0 -0
  62. plexflow/core/metadata/auto/auto_providers/moviemeter/movie.py +40 -0
  63. plexflow/core/metadata/auto/auto_providers/plex/__init__.py +0 -0
  64. plexflow/core/metadata/auto/auto_providers/plex/movie.py +39 -0
  65. plexflow/core/metadata/auto/auto_providers/tmdb/__init__.py +0 -0
  66. plexflow/core/metadata/auto/auto_providers/tmdb/episode.py +30 -0
  67. plexflow/core/metadata/auto/auto_providers/tmdb/movie.py +36 -0
  68. plexflow/core/metadata/auto/auto_providers/tmdb/season.py +23 -0
  69. plexflow/core/metadata/auto/auto_providers/tmdb/show.py +41 -0
  70. plexflow/core/metadata/auto/auto_providers/tmdb.py +92 -0
  71. plexflow/core/metadata/auto/auto_providers/tvdb/__init__.py +0 -0
  72. plexflow/core/metadata/auto/auto_providers/tvdb/episode.py +28 -0
  73. plexflow/core/metadata/auto/auto_providers/tvdb/movie.py +36 -0
  74. plexflow/core/metadata/auto/auto_providers/tvdb/season.py +25 -0
  75. plexflow/core/metadata/auto/auto_providers/tvdb/show.py +41 -0
  76. plexflow/core/metadata/providers/__init__.py +0 -0
  77. plexflow/core/metadata/providers/imdb/__init__.py +0 -0
  78. plexflow/core/metadata/providers/imdb/datatypes.py +53 -0
  79. plexflow/core/metadata/providers/imdb/imdb.py +112 -0
  80. plexflow/core/metadata/providers/moviemeter/__init__.py +0 -0
  81. plexflow/core/metadata/providers/moviemeter/datatypes.py +111 -0
  82. plexflow/core/metadata/providers/moviemeter/moviemeter.py +42 -0
  83. plexflow/core/metadata/providers/plex/__init__.py +0 -0
  84. plexflow/core/metadata/providers/plex/datatypes.py +693 -0
  85. plexflow/core/metadata/providers/plex/plex.py +167 -0
  86. plexflow/core/metadata/providers/tmdb/__init__.py +0 -0
  87. plexflow/core/metadata/providers/tmdb/datatypes.py +460 -0
  88. plexflow/core/metadata/providers/tmdb/tmdb.py +85 -0
  89. plexflow/core/metadata/providers/tvdb/__init__.py +0 -0
  90. plexflow/core/metadata/providers/tvdb/datatypes.py +257 -0
  91. plexflow/core/metadata/providers/tvdb/tv_datatypes.py +554 -0
  92. plexflow/core/metadata/providers/tvdb/tvdb.py +65 -0
  93. plexflow/core/metadata/providers/universal/__init__.py +0 -0
  94. plexflow/core/metadata/providers/universal/movie.py +130 -0
  95. plexflow/core/metadata/providers/universal/old.py +192 -0
  96. plexflow/core/metadata/providers/universal/show.py +107 -0
  97. plexflow/core/plex/__init__.py +0 -0
  98. plexflow/core/plex/api/context/authorized.py +15 -0
  99. plexflow/core/plex/api/context/discover.py +14 -0
  100. plexflow/core/plex/api/context/library.py +14 -0
  101. plexflow/core/plex/discover/__init__.py +0 -0
  102. plexflow/core/plex/discover/activity.py +448 -0
  103. plexflow/core/plex/discover/comment.py +89 -0
  104. plexflow/core/plex/discover/feed.py +11 -0
  105. plexflow/core/plex/hooks/__init__.py +0 -0
  106. plexflow/core/plex/hooks/plex_authorized.py +60 -0
  107. plexflow/core/plex/hooks/plexflow_database.py +6 -0
  108. plexflow/core/plex/library/__init__.py +0 -0
  109. plexflow/core/plex/library/library.py +103 -0
  110. plexflow/core/plex/token/__init__.py +0 -0
  111. plexflow/core/plex/token/auto_token.py +91 -0
  112. plexflow/core/plex/utils/__init__.py +0 -0
  113. plexflow/core/plex/utils/paginated.py +39 -0
  114. plexflow/core/plex/watchlist/__init__.py +0 -0
  115. plexflow/core/plex/watchlist/datatypes.py +124 -0
  116. plexflow/core/plex/watchlist/watchlist.py +23 -0
  117. plexflow/core/storage/__init__.py +0 -0
  118. plexflow/core/storage/object/__init__.py +0 -0
  119. plexflow/core/storage/object/plexflow_storage.py +143 -0
  120. plexflow/core/storage/object/redis_storage.py +169 -0
  121. plexflow/core/subtitles/__init__.py +0 -0
  122. plexflow/core/subtitles/providers/__init__.py +0 -0
  123. plexflow/core/subtitles/providers/auto_subtitles.py +48 -0
  124. plexflow/core/subtitles/providers/oss/__init__.py +0 -0
  125. plexflow/core/subtitles/providers/oss/datatypes.py +104 -0
  126. plexflow/core/subtitles/providers/oss/download.py +48 -0
  127. plexflow/core/subtitles/providers/oss/old.py +144 -0
  128. plexflow/core/subtitles/providers/oss/oss.py +400 -0
  129. plexflow/core/subtitles/providers/oss/oss_subtitle.py +32 -0
  130. plexflow/core/subtitles/providers/oss/search.py +52 -0
  131. plexflow/core/subtitles/providers/oss/unlimited_oss.py +231 -0
  132. plexflow/core/subtitles/providers/oss/utils/__init__.py +0 -0
  133. plexflow/core/subtitles/providers/oss/utils/config.py +63 -0
  134. plexflow/core/subtitles/providers/oss/utils/download_client.py +22 -0
  135. plexflow/core/subtitles/providers/oss/utils/exceptions.py +35 -0
  136. plexflow/core/subtitles/providers/oss/utils/file_utils.py +83 -0
  137. plexflow/core/subtitles/providers/oss/utils/languages.py +78 -0
  138. plexflow/core/subtitles/providers/oss/utils/response_base.py +221 -0
  139. plexflow/core/subtitles/providers/oss/utils/responses.py +176 -0
  140. plexflow/core/subtitles/providers/oss/utils/srt.py +561 -0
  141. plexflow/core/subtitles/results/__init__.py +0 -0
  142. plexflow/core/subtitles/results/subtitle.py +170 -0
  143. plexflow/core/torrents/__init__.py +0 -0
  144. plexflow/core/torrents/analyzers/analyzed_torrent.py +143 -0
  145. plexflow/core/torrents/analyzers/analyzer.py +45 -0
  146. plexflow/core/torrents/analyzers/torrentquest/analyzer.py +47 -0
  147. plexflow/core/torrents/auto/auto_providers/auto/__init__.py +0 -0
  148. plexflow/core/torrents/auto/auto_providers/auto/torrent.py +64 -0
  149. plexflow/core/torrents/auto/auto_providers/tpb/torrent.py +62 -0
  150. plexflow/core/torrents/auto/auto_torrents.py +29 -0
  151. plexflow/core/torrents/providers/__init__.py +0 -0
  152. plexflow/core/torrents/providers/ext/__init__.py +0 -0
  153. plexflow/core/torrents/providers/ext/ext.py +18 -0
  154. plexflow/core/torrents/providers/ext/utils.py +64 -0
  155. plexflow/core/torrents/providers/extratorrent/__init__.py +0 -0
  156. plexflow/core/torrents/providers/extratorrent/extratorrent.py +21 -0
  157. plexflow/core/torrents/providers/extratorrent/utils.py +66 -0
  158. plexflow/core/torrents/providers/eztv/__init__.py +0 -0
  159. plexflow/core/torrents/providers/eztv/eztv.py +47 -0
  160. plexflow/core/torrents/providers/eztv/utils.py +83 -0
  161. plexflow/core/torrents/providers/rarbg2/__init__.py +0 -0
  162. plexflow/core/torrents/providers/rarbg2/rarbg2.py +19 -0
  163. plexflow/core/torrents/providers/rarbg2/utils.py +76 -0
  164. plexflow/core/torrents/providers/snowfl/__init__.py +0 -0
  165. plexflow/core/torrents/providers/snowfl/snowfl.py +36 -0
  166. plexflow/core/torrents/providers/snowfl/utils.py +59 -0
  167. plexflow/core/torrents/providers/tgx/__init__.py +0 -0
  168. plexflow/core/torrents/providers/tgx/context.py +50 -0
  169. plexflow/core/torrents/providers/tgx/dump.py +40 -0
  170. plexflow/core/torrents/providers/tgx/tgx.py +22 -0
  171. plexflow/core/torrents/providers/tgx/utils.py +61 -0
  172. plexflow/core/torrents/providers/therarbg/__init__.py +0 -0
  173. plexflow/core/torrents/providers/therarbg/therarbg.py +17 -0
  174. plexflow/core/torrents/providers/therarbg/utils.py +61 -0
  175. plexflow/core/torrents/providers/torrentquest/__init__.py +0 -0
  176. plexflow/core/torrents/providers/torrentquest/torrentquest.py +20 -0
  177. plexflow/core/torrents/providers/torrentquest/utils.py +70 -0
  178. plexflow/core/torrents/providers/tpb/__init__.py +0 -0
  179. plexflow/core/torrents/providers/tpb/tpb.py +17 -0
  180. plexflow/core/torrents/providers/tpb/utils.py +139 -0
  181. plexflow/core/torrents/providers/yts/__init__.py +0 -0
  182. plexflow/core/torrents/providers/yts/utils.py +57 -0
  183. plexflow/core/torrents/providers/yts/yts.py +31 -0
  184. plexflow/core/torrents/results/__init__.py +0 -0
  185. plexflow/core/torrents/results/torrent.py +165 -0
  186. plexflow/core/torrents/results/universal.py +220 -0
  187. plexflow/core/torrents/results/utils.py +15 -0
  188. plexflow/events/__init__.py +0 -0
  189. plexflow/events/download/__init__.py +0 -0
  190. plexflow/events/download/torrent_events.py +96 -0
  191. plexflow/events/publish/__init__.py +0 -0
  192. plexflow/events/publish/publish.py +34 -0
  193. plexflow/logging/__init__.py +0 -0
  194. plexflow/logging/log_setup.py +8 -0
  195. plexflow/spiders/quiet_logger.py +9 -0
  196. plexflow/spiders/tgx/pipelines/dump_json_pipeline.py +30 -0
  197. plexflow/spiders/tgx/pipelines/meta_pipeline.py +13 -0
  198. plexflow/spiders/tgx/pipelines/publish_pipeline.py +14 -0
  199. plexflow/spiders/tgx/pipelines/torrent_info_pipeline.py +12 -0
  200. plexflow/spiders/tgx/pipelines/validation_pipeline.py +17 -0
  201. plexflow/spiders/tgx/settings.py +36 -0
  202. plexflow/spiders/tgx/spider.py +72 -0
  203. plexflow/utils/__init__.py +0 -0
  204. plexflow/utils/antibot/human_like_requests.py +122 -0
  205. plexflow/utils/api/__init__.py +0 -0
  206. plexflow/utils/api/context/http.py +62 -0
  207. plexflow/utils/api/rest/__init__.py +0 -0
  208. plexflow/utils/api/rest/antibot_restful.py +68 -0
  209. plexflow/utils/api/rest/restful.py +49 -0
  210. plexflow/utils/captcha/__init__.py +0 -0
  211. plexflow/utils/captcha/bypass/__init__.py +0 -0
  212. plexflow/utils/captcha/bypass/decode_audio.py +34 -0
  213. plexflow/utils/download/__init__.py +0 -0
  214. plexflow/utils/download/gz.py +26 -0
  215. plexflow/utils/filesystem/__init__.py +0 -0
  216. plexflow/utils/filesystem/search.py +129 -0
  217. plexflow/utils/gmail/__init__.py +0 -0
  218. plexflow/utils/gmail/mails.py +116 -0
  219. plexflow/utils/hooks/__init__.py +0 -0
  220. plexflow/utils/hooks/http.py +84 -0
  221. plexflow/utils/hooks/postgresql.py +93 -0
  222. plexflow/utils/hooks/redis.py +112 -0
  223. plexflow/utils/image/storage.py +36 -0
  224. plexflow/utils/imdb/__init__.py +0 -0
  225. plexflow/utils/imdb/imdb_codes.py +107 -0
  226. plexflow/utils/pubsub/consume.py +82 -0
  227. plexflow/utils/pubsub/produce.py +25 -0
  228. plexflow/utils/retry/__init__.py +0 -0
  229. plexflow/utils/retry/utils.py +38 -0
  230. plexflow/utils/strings/__init__.py +0 -0
  231. plexflow/utils/strings/filesize.py +55 -0
  232. plexflow/utils/strings/language.py +14 -0
  233. plexflow/utils/subtitle/search.py +76 -0
  234. plexflow/utils/tasks/decorators.py +78 -0
  235. plexflow/utils/tasks/k8s/task.py +70 -0
  236. plexflow/utils/thread_safe/safe_list.py +54 -0
  237. plexflow/utils/thread_safe/safe_set.py +69 -0
  238. plexflow/utils/torrent/__init__.py +0 -0
  239. plexflow/utils/torrent/analyze.py +118 -0
  240. plexflow/utils/torrent/extract/common.py +37 -0
  241. plexflow/utils/torrent/extract/ext.py +2391 -0
  242. plexflow/utils/torrent/extract/extratorrent.py +56 -0
  243. plexflow/utils/torrent/extract/kat.py +1581 -0
  244. plexflow/utils/torrent/extract/tgx.py +96 -0
  245. plexflow/utils/torrent/extract/therarbg.py +170 -0
  246. plexflow/utils/torrent/extract/torrentquest.py +171 -0
  247. plexflow/utils/torrent/files.py +36 -0
  248. plexflow/utils/torrent/hash.py +90 -0
  249. plexflow/utils/transcribe/__init__.py +0 -0
  250. plexflow/utils/transcribe/speech2text.py +40 -0
  251. plexflow/utils/video/__init__.py +0 -0
  252. plexflow/utils/video/subtitle.py +73 -0
  253. plexflow-0.0.64.dist-info/METADATA +71 -0
  254. plexflow-0.0.64.dist-info/RECORD +256 -0
  255. plexflow-0.0.64.dist-info/WHEEL +4 -0
  256. plexflow-0.0.64.dist-info/entry_points.txt +24 -0
@@ -0,0 +1,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)