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,167 @@
1
+ from typing import Union, List
2
+ from plexflow.core.plex.hooks.plex_authorized import PlexAuthorizedHttpHook
3
+ from plexflow.core.plex.api.context.discover import PlexDiscoverRequestContext
4
+ from plexflow.core.metadata.providers.plex.datatypes import MovieMediaContainer, PlexMovieMetadata, ShowMediaContainer, PlexShowMetadata, PlexSeasonMetadata, SeasonMediaContainer, PlexEpisodeMetadata, EpisodeMediaContainer, PlexUserState, WatchStateMediaContainer
5
+
6
+ def search_movie_by_rating_key(key: str) -> Union[PlexMovieMetadata, None]:
7
+ """
8
+ Search for a movie using its rating key.
9
+
10
+ Parameters:
11
+ key (str): The rating key of the movie.
12
+
13
+ Returns:
14
+ Union[PlexMovieMetadata, None]: A PlexMovieMetadata object containing all the details of the movie.
15
+ None: If the movie is not found or an error occurs.
16
+
17
+ Example:
18
+ >>> search_movie_by_rating_key('12345')
19
+ PlexMovieMetadata(title='The Shawshank Redemption', ratingKey='12345', release_date=datetime.datetime(1994, 9, 23, 0, 0), runtime=8520, rating=9.3, votes=2400000, rank=1)
20
+ """
21
+ context = PlexDiscoverRequestContext()
22
+ response = context.get(f"/library/metadata/{key}")
23
+
24
+ response.raise_for_status()
25
+
26
+ data = response.json()
27
+
28
+ # import pprint as pp
29
+ # pp.pprint(data.get("MediaContainer"))
30
+ container: MovieMediaContainer = MovieMediaContainer.from_dict(data.get("MediaContainer"))
31
+
32
+ if len(container.Metadata) > 0:
33
+ return container.Metadata[0]
34
+
35
+ def search_show_by_rating_key(key: str) -> Union[PlexShowMetadata, None]:
36
+ """
37
+ Search for a show using its rating key.
38
+
39
+ Parameters:
40
+ key (str): The rating key of the show.
41
+
42
+ Returns:
43
+ Union[PlexShowMetadata, None]: A PlexShowMetadata object containing all the details of the show.
44
+ None: If the show is not found or an error occurs.
45
+
46
+ Example:
47
+ >>> search_show_by_rating_key('12345')
48
+ PlexShowMetadata(title='Friends', ratingKey='12345', parentRatingKey='67890')
49
+ """
50
+ context = PlexDiscoverRequestContext()
51
+ response = context.get(f"/library/metadata/{key}")
52
+
53
+ response.raise_for_status()
54
+
55
+ data = response.json()
56
+
57
+ container: ShowMediaContainer = ShowMediaContainer.from_dict(data.get("MediaContainer"))
58
+
59
+ if len(container.Metadata) > 0:
60
+ return container.Metadata[0]
61
+
62
+
63
+ def search_seasons_by_show_rating_key(key: str) -> Union[List[PlexSeasonMetadata], None]:
64
+ """
65
+ Search for seasons of a show using its rating key.
66
+
67
+ Parameters:
68
+ key (str): The rating key of the show.
69
+
70
+ Returns:
71
+ List[PlexSeasonMetadata]: A list of PlexSeasonMetadata objects containing the details of the seasons.
72
+ None: If the show is not found or an error occurs.
73
+
74
+ Example:
75
+ >>> search_seasons_by_show_rating_key('12345')
76
+ [PlexSeasonMetadata(title='Season 1', ratingKey='123', parentRatingKey='456'), PlexSeasonMetadata(title='Season 2', ratingKey='789', parentRatingKey='456')]
77
+ """
78
+ context = PlexDiscoverRequestContext()
79
+ response = context.get(f"/library/metadata/{key}/children")
80
+
81
+ response.raise_for_status()
82
+
83
+ data = response.json()
84
+
85
+ container: SeasonMediaContainer = SeasonMediaContainer.from_dict(data.get("MediaContainer"))
86
+
87
+ if len(container.Metadata) > 0:
88
+ return container.Metadata
89
+
90
+
91
+ def search_episodes_by_season_rating_key(key: str) -> Union[List[PlexEpisodeMetadata], None]:
92
+ """
93
+ Search for episodes of a season using its rating key.
94
+
95
+ Parameters:
96
+ key (str): The rating key of the season.
97
+
98
+ Returns:
99
+ List[PlexEpisodeMetadata]: A list of PlexEpisodeMetadata objects containing the details of the episodes.
100
+ None: If the season is not found or an error occurs.
101
+
102
+ Example:
103
+ >>> search_episodes_by_season_rating_key('12345')
104
+ [PlexEpisodeMetadata(title='Episode 1', duration=3600, rating=8.5), PlexEpisodeMetadata(title='Episode 2', duration=3600, rating=9.0)]
105
+ """
106
+ context = PlexDiscoverRequestContext()
107
+ response = context.get(f"/library/metadata/{key}/children")
108
+
109
+ response.raise_for_status()
110
+
111
+ data = response.json()
112
+
113
+ container: EpisodeMediaContainer = EpisodeMediaContainer.from_dict(data.get("MediaContainer"))
114
+
115
+ if len(container.Metadata) > 0:
116
+ return container.Metadata
117
+
118
+ def get_all_episodes_by_show_rating_key(key: str) -> List[PlexEpisodeMetadata]:
119
+ """
120
+ Get all episodes of a show using its rating key.
121
+
122
+ Parameters:
123
+ key (str): The rating key of the show.
124
+
125
+ Returns:
126
+ List[PlexEpisodeMetadata]: A list of PlexEpisodeMetadata objects containing the details of the episodes.
127
+
128
+ Example:
129
+ >>> get_all_episodes_by_show_rating_key('12345')
130
+ [PlexEpisodeMetadata(title='Episode 1', duration=3600, rating=8.5), PlexEpisodeMetadata(title='Episode 2', duration=3600, rating=9.0)]
131
+ """
132
+ show = search_show_by_rating_key(key=key)
133
+
134
+ seasons = search_seasons_by_show_rating_key(show.ratingKey)
135
+
136
+ episodes = []
137
+ for season in seasons:
138
+ tmp = search_episodes_by_season_rating_key(season.ratingKey)
139
+ for x in tmp:
140
+ episodes.append(x)
141
+ return episodes
142
+
143
+ def get_watch_state_by_rating_key(key: str) -> Union[PlexUserState, None]:
144
+ """
145
+ Get the watch state of a movie or show using its rating key.
146
+
147
+ Parameters:
148
+ key (str): The rating key of the movie or show.
149
+
150
+ Returns:
151
+ PlexUserState: A PlexUserState object containing the watch state.
152
+ None: If the movie or show is not found or an error occurs.
153
+
154
+ Example:
155
+ >>> get_watch_state_by_rating_key('12345')
156
+ PlexUserState(state='watched', last_viewed_at=datetime.datetime(2022, 1, 1, 0, 0))
157
+ """
158
+ context = PlexDiscoverRequestContext()
159
+ response = context.get(f"/library/metadata/{key}/userState")
160
+
161
+ response.raise_for_status()
162
+
163
+ data = response.json()
164
+
165
+ container: WatchStateMediaContainer = WatchStateMediaContainer.from_dict(data.get("MediaContainer"))
166
+
167
+ return container.UserState
File without changes
@@ -0,0 +1,460 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import List, Optional, Dict
3
+ from dataclasses_json import dataclass_json, Undefined
4
+ import json
5
+
6
+ @dataclass_json(undefined=Undefined.EXCLUDE)
7
+ @dataclass
8
+ class Genre:
9
+ """
10
+ Represents a genre of a movie.
11
+
12
+ Attributes:
13
+ id (int): The unique identifier of the genre.
14
+ name (str): The name of the genre.
15
+ """
16
+ id: Optional[int] = None
17
+ name: Optional[str] = None
18
+ _catchall: Dict[str, str] = field(default_factory=dict)
19
+
20
+ @dataclass_json(undefined=Undefined.EXCLUDE)
21
+ @dataclass
22
+ class ProductionCompany:
23
+ """
24
+ Represents a production company of a movie.
25
+
26
+ Attributes:
27
+ id (int): The unique identifier of the production company.
28
+ logo_path (Optional[str]): The path to the logo of the production company.
29
+ name (str): The name of the production company.
30
+ origin_country (str): The country of origin of the production company.
31
+ """
32
+ id: Optional[int] = None
33
+ logo_path: Optional[str] = None
34
+ name: Optional[str] = None
35
+ origin_country: Optional[str] = None
36
+ _catchall: Dict[str, str] = field(default_factory=dict)
37
+
38
+ @dataclass_json(undefined=Undefined.EXCLUDE)
39
+ @dataclass
40
+ class ProductionCountry:
41
+ """
42
+ Represents a production country of a movie.
43
+
44
+ Attributes:
45
+ iso_3166_1 (str): The ISO 3166-1 alpha-2 code of the country.
46
+ name (str): The name of the country.
47
+ """
48
+ iso_3166_1: Optional[str] = None
49
+ name: Optional[str] = None
50
+ _catchall: Dict[str, str] = field(default_factory=dict)
51
+
52
+ @dataclass_json(undefined=Undefined.EXCLUDE)
53
+ @dataclass
54
+ class SpokenLanguage:
55
+ """
56
+ Represents a language spoken in a movie.
57
+
58
+ Attributes:
59
+ english_name (str): The English name of the language.
60
+ iso_639_1 (str): The ISO 639-1 code of the language.
61
+ name (str): The name of the language in the original language.
62
+ """
63
+ english_name: Optional[str] = None
64
+ iso_639_1: Optional[str] = None
65
+ name: Optional[str] = None
66
+ _catchall: Dict[str, str] = field(default_factory=dict)
67
+
68
+ @dataclass_json(undefined=Undefined.EXCLUDE)
69
+ @dataclass
70
+ class AlternativeTitle:
71
+ """
72
+ Represents an alternative title of a movie.
73
+
74
+ Attributes:
75
+ iso_3166_1 (str): The ISO 3166-1 alpha-2 code of the country where the alternative title is used.
76
+ title (str): The alternative title.
77
+ type (str): The type of the alternative title.
78
+ """
79
+ iso_3166_1: Optional[str] = None
80
+ title: Optional[str] = None
81
+ type: Optional[str] = None
82
+ _catchall: Dict[str, str] = field(default_factory=dict)
83
+
84
+ @dataclass_json(undefined=Undefined.EXCLUDE)
85
+ @dataclass
86
+ class AlternativeTitles:
87
+ """
88
+ Represents a collection of alternative titles of a movie.
89
+
90
+ Attributes:
91
+ titles (List[AlternativeTitle]): The list of alternative titles.
92
+ """
93
+ titles: Optional[List[AlternativeTitle]] = None
94
+ _catchall: Dict[str, str] = field(default_factory=dict)
95
+
96
+ def __post_init__(self):
97
+ self.titles = [AlternativeTitle(**title) if isinstance(title, dict) else title for title in self.titles]
98
+
99
+ @dataclass_json(undefined=Undefined.EXCLUDE)
100
+ @dataclass
101
+ class AlternativeTitlesShow:
102
+ """
103
+ Represents a collection of alternative titles of a movie.
104
+
105
+ Attributes:
106
+ results (List[AlternativeTitle]): The list of alternative titles.
107
+ """
108
+ results: Optional[List[AlternativeTitle]] = None
109
+ _catchall: Dict[str, str] = field(default_factory=dict)
110
+
111
+ def __post_init__(self):
112
+ self.results = [AlternativeTitle(**title) if isinstance(title, dict) else title for title in self.results]
113
+
114
+ @dataclass_json(undefined=Undefined.EXCLUDE)
115
+ @dataclass
116
+ class TmdbMovie:
117
+ """
118
+ Represents a movie.
119
+
120
+ Attributes:
121
+ adult (bool): Whether the movie is for adults.
122
+ backdrop_path (str): The path to the backdrop image of the movie.
123
+ belongs_to_collection (Optional[str]): The collection the movie belongs to.
124
+ budget (int): The budget of the movie.
125
+ genres (List[Genre]): The genres of the movie.
126
+ homepage (str): The homepage of the movie.
127
+ id (int): The unique identifier of the movie.
128
+ imdb_id (str): The IMDb identifier of the movie.
129
+ original_language (str): The original language of the movie.
130
+ original_title (str): The original title of the movie.
131
+ overview (str): The overview of the movie.
132
+ popularity (float): The popularity of the movie.
133
+ poster_path (str): The path to the poster image of the movie.
134
+ production_companies (List[ProductionCompany]): The production companies of the movie.
135
+ production_countries (List[ProductionCountry]): The production countries of the movie.
136
+ release_date (str): The release date of the movie.
137
+ revenue (int): The revenue of the movie.
138
+ runtime (int): The runtime of the movie.
139
+ spoken_languages (List[SpokenLanguage]): The languages spoken in the movie.
140
+ status (str): The status of the movie.
141
+ tagline (str): The tagline of the movie.
142
+ title (str): The title of the movie.
143
+ video (bool): Whether the movie has a video.
144
+ vote_average (float): The average vote of the movie.
145
+ vote_count (int): The vote count of the movie.
146
+ alternative_titles (AlternativeTitles): The alternative titles of the movie.
147
+ _catchall (dict): A dictionary to catch all other fields not explicitly defined in the data class.
148
+ """
149
+ adult: Optional[bool] = None
150
+ backdrop_path: Optional[str] = None
151
+ belongs_to_collection: Optional[str] = None
152
+ budget: Optional[int] = None
153
+ genres: Optional[List[Genre]] = None
154
+ homepage: Optional[str] = None
155
+ id: Optional[int] = None
156
+ imdb_id: Optional[str] = None
157
+ original_language: Optional[str] = None
158
+ original_title: Optional[str] = None
159
+ overview: Optional[str] = None
160
+ popularity: Optional[float] = None
161
+ poster_path: Optional[str] = None
162
+ production_companies: Optional[List[ProductionCompany]] = None
163
+ production_countries: Optional[List[ProductionCountry]] = None
164
+ release_date: Optional[str] = None
165
+ revenue: Optional[int] = None
166
+ runtime: Optional[int] = None
167
+ spoken_languages: Optional[List[SpokenLanguage]] = None
168
+ status: Optional[str] = None
169
+ tagline: Optional[str] = None
170
+ title: Optional[str] = None
171
+ video: Optional[bool] = None
172
+ vote_average: Optional[float] = None
173
+ vote_count: Optional[int] = None
174
+ alternative_titles: Optional[AlternativeTitles] = None
175
+ _catchall: Dict[str, str] = field(default_factory=dict)
176
+
177
+ def __post_init__(self):
178
+ self.genres = [Genre(**genre) if isinstance(genre, dict) else genre for genre in self.genres]
179
+ self.production_companies = [ProductionCompany(**pc) if isinstance(pc, dict) else pc for pc in self.production_companies]
180
+ self.production_countries = [ProductionCountry(**pc) if isinstance(pc, dict) else pc for pc in self.production_countries]
181
+ self.spoken_languages = [SpokenLanguage(**sl) if isinstance(sl, dict) else sl for sl in self.spoken_languages]
182
+ self.alternative_titles = AlternativeTitles(**self.alternative_titles) if isinstance(self.alternative_titles, dict) else self.alternative_titles
183
+
184
+ @dataclass_json(undefined=Undefined.EXCLUDE)
185
+ @dataclass
186
+ class Creator:
187
+ """
188
+ Represents a creator of a TV show.
189
+
190
+ Attributes:
191
+ id (int): The unique identifier of the creator.
192
+ credit_id (str): The credit identifier of the creator.
193
+ name (str): The name of the creator.
194
+ gender (int): The gender of the creator.
195
+ profile_path (str): The path to the profile of the creator.
196
+ """
197
+ id: Optional[int] = None
198
+ credit_id: Optional[str] = None
199
+ name: Optional[str] = None
200
+ gender: Optional[int] = None
201
+ profile_path: Optional[str] = None
202
+ _catchall: Dict[str, str] = field(default_factory=dict)
203
+
204
+ @dataclass_json(undefined=Undefined.EXCLUDE)
205
+ @dataclass
206
+ class Network:
207
+ """
208
+ Represents a network of a TV show.
209
+
210
+ Attributes:
211
+ id (int): The unique identifier of the network.
212
+ logo_path (str): The path to the logo of the network.
213
+ name (str): The name of the network.
214
+ origin_country (str): The country of origin of the network.
215
+ """
216
+ id: Optional[int] = None
217
+ logo_path: Optional[str] = None
218
+ name: Optional[str] = None
219
+ origin_country: Optional[str] = None
220
+ _catchall: Dict[str, str] = field(default_factory=dict)
221
+
222
+ @dataclass_json(undefined=Undefined.EXCLUDE)
223
+ @dataclass
224
+ class Season:
225
+ """
226
+ Represents a season of a TV show.
227
+
228
+ Attributes:
229
+ air_date (str): The air date of the season.
230
+ episode_count (int): The number of episodes in the season.
231
+ id (int): The unique identifier of the season.
232
+ name (str): The name of the season.
233
+ overview (str): The overview of the season.
234
+ poster_path (str): The path to the poster of the season.
235
+ season_number (int): The number of the season.
236
+ vote_average (float): The average vote of the season.
237
+ """
238
+ air_date: Optional[str] = None
239
+ episode_count: Optional[int] = None
240
+ id: Optional[int] = None
241
+ name: Optional[str] = None
242
+ overview: Optional[str] = None
243
+ poster_path: Optional[str] = None
244
+ season_number: Optional[int] = None
245
+ vote_average: Optional[float] = None
246
+ _catchall: Dict[str, str] = field(default_factory=dict)
247
+
248
+ @dataclass_json(undefined=Undefined.EXCLUDE)
249
+ @dataclass
250
+ class Episode:
251
+ """
252
+ Represents an episode of a TV show.
253
+
254
+ Attributes:
255
+ id (int): The unique identifier of the episode.
256
+ name (str): The name of the episode.
257
+ overview (str): The overview of the episode.
258
+ vote_average (float): The average vote of the episode.
259
+ vote_count (int): The vote count of the episode.
260
+ air_date (str): The air date of the episode.
261
+ episode_number (int): The number of the episode.
262
+ episode_type (str): The type of the episode.
263
+ production_code (str): The production code of the episode.
264
+ runtime (int): The runtime of the episode.
265
+ season_number (int): The number of the season.
266
+ show_id (int): The id of the show.
267
+ still_path (str): The path to the still of the episode.
268
+ """
269
+ id: Optional[int] = None
270
+ name: Optional[str] = None
271
+ overview: Optional[str] = None
272
+ vote_average: Optional[float] = None
273
+ vote_count: Optional[int] = None
274
+ air_date: Optional[str] = None
275
+ episode_number: Optional[int] = None
276
+ episode_type: Optional[str] = None
277
+ production_code: Optional[str] = None
278
+ runtime: Optional[int] = None
279
+ season_number: Optional[int] = None
280
+ show_id: Optional[int] = None
281
+ still_path: Optional[str] = None
282
+ _catchall: Dict[str, str] = field(default_factory=dict)
283
+
284
+ @dataclass_json(undefined=Undefined.EXCLUDE)
285
+ @dataclass
286
+ class ExternalIds:
287
+ """
288
+ Represents the external IDs of a TV show.
289
+
290
+ Attributes:
291
+ imdb_id (str): The IMDb ID of the TV show.
292
+ freebase_mid (str): The Freebase MID of the TV show.
293
+ freebase_id (str): The Freebase ID of the TV show.
294
+ tvdb_id (int): The TVDB ID of the TV show.
295
+ tvrage_id (int): The TVRage ID of the TV show.
296
+ wikidata_id (str): The Wikidata ID of the TV show.
297
+ facebook_id (str): The Facebook ID of the TV show.
298
+ instagram_id (str): The Instagram ID of the TV show.
299
+ twitter_id (str): The Twitter ID of the TV show.
300
+ """
301
+ imdb_id: Optional[str] = None
302
+ freebase_mid: Optional[str] = None
303
+ freebase_id: Optional[str] = None
304
+ tvdb_id: Optional[int] = None
305
+ tvrage_id: Optional[int] = None
306
+ wikidata_id: Optional[str] = None
307
+ facebook_id: Optional[str] = None
308
+ instagram_id: Optional[str] = None
309
+ twitter_id: Optional[str] = None
310
+ _catchall: Dict[str, str] = field(default_factory=dict)
311
+
312
+
313
+ @dataclass_json(undefined=Undefined.EXCLUDE)
314
+ @dataclass
315
+ class TmdbShow:
316
+ """
317
+ Represents a TV show.
318
+
319
+ Attributes:
320
+ created_by (List[Creator]): The creators of the TV show.
321
+ episode_run_time (List[int]): The runtime of the episodes of the TV show.
322
+ first_air_date (str): The first air date of the TV show.
323
+ in_production (bool): Whether the TV show is in production.
324
+ languages (List[str]): The languages of the TV show.
325
+ last_air_date (str): The last air date of the TV show.
326
+ last_episode_to_air (Episode): The last episode to air.
327
+ networks (List[Network]): The networks of the TV show.
328
+ number_of_episodes (int): The number of episodes of the TV show.
329
+ number_of_seasons (int): The number of seasons of the TV show.
330
+ origin_country (List[str]): The countries of origin of the TV show.
331
+ original_name (str): The original name of the TV show.
332
+ seasons (List[Season]): The seasons of the TV show.
333
+ type (str): The type of the TV show.
334
+ """
335
+ created_by: Optional[List[Creator]] = None
336
+ episode_run_time: Optional[List[int]] = None
337
+ first_air_date: Optional[str] = None
338
+ in_production: Optional[bool] = None
339
+ languages: Optional[List[str]] = None
340
+ last_air_date: Optional[str] = None
341
+ last_episode_to_air: Optional[Episode] = None
342
+ networks: Optional[List[Network]] = None
343
+ number_of_episodes: Optional[int] = None
344
+ number_of_seasons: Optional[int] = None
345
+ origin_country: Optional[List[str]] = None
346
+ original_name: Optional[str] = None
347
+ seasons: Optional[List[Season]] = None
348
+ type: Optional[str] = None
349
+ adult: Optional[bool] = None
350
+ backdrop_path: Optional[str] = None
351
+ belongs_to_collection: Optional[str] = None
352
+ budget: Optional[int] = None
353
+ genres: Optional[List[Genre]] = None
354
+ homepage: Optional[str] = None
355
+ id: Optional[int] = None
356
+ imdb_id: Optional[str] = None
357
+ original_language: Optional[str] = None
358
+ overview: Optional[str] = None
359
+ popularity: Optional[float] = None
360
+ poster_path: Optional[str] = None
361
+ production_companies: Optional[List[ProductionCompany]] = None
362
+ production_countries: Optional[List[ProductionCountry]] = None
363
+ spoken_languages: Optional[List[SpokenLanguage]] = None
364
+ status: Optional[str] = None
365
+ tagline: Optional[str] = None
366
+ vote_average: Optional[float] = None
367
+ vote_count: Optional[int] = None
368
+ alternative_titles: Optional[AlternativeTitlesShow] = None
369
+ external_ids: Optional[ExternalIds] = None
370
+
371
+ def __post_init__(self):
372
+ self.created_by = [Creator(**creator) if isinstance(creator, dict) else creator for creator in self.created_by]
373
+ self.last_episode_to_air = Episode(**self.last_episode_to_air) if isinstance(self.last_episode_to_air, dict) else self.last_episode_to_air
374
+ self.networks = [Network(**network) if isinstance(network, dict) else network for network in self.networks]
375
+ self.seasons = [Season(**season) if isinstance(season, dict) else season for season in self.seasons]
376
+
377
+ self.genres = [Genre(**genre) if isinstance(genre, dict) else genre for genre in self.genres]
378
+ self.production_companies = [ProductionCompany(**pc) if isinstance(pc, dict) else pc for pc in self.production_companies]
379
+ self.production_countries = [ProductionCountry(**pc) if isinstance(pc, dict) else pc for pc in self.production_countries]
380
+ self.spoken_languages = [SpokenLanguage(**sl) if isinstance(sl, dict) else sl for sl in self.spoken_languages]
381
+ self.alternative_titles = AlternativeTitlesShow(**self.alternative_titles) if isinstance(self.alternative_titles, dict) else self.alternative_titles
382
+ self.external_ids = ExternalIds(**self.external_ids) if isinstance(self.external_ids, dict) else self.external_ids
383
+
384
+
385
+
386
+
387
+ from dataclasses import dataclass, field
388
+ from typing import List, Optional
389
+ from dataclasses_json import dataclass_json, Undefined
390
+
391
+ @dataclass_json(undefined=Undefined.EXCLUDE)
392
+ @dataclass
393
+ class TmdbPerson:
394
+ job: Optional[str] = None
395
+ department: Optional[str] = None
396
+ credit_id: Optional[str] = None
397
+ adult: Optional[bool] = None
398
+ gender: Optional[int] = None
399
+ id: Optional[int] = None
400
+ known_for_department: Optional[str] = None
401
+ name: Optional[str] = None
402
+ original_name: Optional[str] = None
403
+ popularity: Optional[float] = None
404
+ profile_path: Optional[str] = None
405
+
406
+ @dataclass_json(undefined=Undefined.EXCLUDE)
407
+ @dataclass
408
+ class SeasonEpisode:
409
+ air_date: Optional[str] = None
410
+ episode_number: Optional[int] = None
411
+ episode_type: Optional[str] = None
412
+ id: Optional[int] = None
413
+ name: Optional[str] = None
414
+ overview: Optional[str] = None
415
+ production_code: Optional[str] = None
416
+ runtime: Optional[int] = None
417
+ season_number: Optional[int] = None
418
+ show_id: Optional[int] = None
419
+ still_path: Optional[str] = None
420
+ vote_average: Optional[float] = None
421
+ vote_count: Optional[int] = None
422
+ crew: Optional[List[TmdbPerson]] = None
423
+ guest_stars: Optional[List[TmdbPerson]] = None
424
+
425
+ def __post_init__(self):
426
+ self.crew = [TmdbPerson(**creator) if isinstance(creator, dict) else creator for creator in self.crew]
427
+ self.guest_stars = [TmdbPerson(**creator) if isinstance(creator, dict) else creator for creator in self.guest_stars]
428
+
429
+ @dataclass_json(undefined=Undefined.EXCLUDE)
430
+ @dataclass
431
+ class Network:
432
+ name: Optional[str] = None
433
+ id: Optional[int] = None
434
+ logo_path: Optional[str] = None
435
+ origin_country: Optional[str] = None
436
+
437
+ @dataclass_json(undefined=Undefined.EXCLUDE)
438
+ @dataclass
439
+ class TmdbSeason:
440
+ air_date: Optional[str] = None
441
+ id: Optional[int] = None
442
+ name: Optional[str] = None
443
+ season_number: Optional[int] = None
444
+ vote_average: Optional[float] = None
445
+ episodes: Optional[List[SeasonEpisode]] = None
446
+
447
+ def __post_init__(self):
448
+ self.episodes = [SeasonEpisode(**episode) if isinstance(episode, dict) else episode for episode in self.episodes]
449
+
450
+ def show_from_json(json_str: str) -> TmdbShow:
451
+ try:
452
+ return TmdbShow(**json.loads(json_str))
453
+ except json.JSONDecodeError as e:
454
+ raise f"Error decoding JSON: {e}"
455
+
456
+ def movie_from_json(json_str: str) -> TmdbMovie:
457
+ try:
458
+ return TmdbMovie(**json.loads(json_str))
459
+ except json.JSONDecodeError as e:
460
+ raise f"Error decoding JSON: {e}"