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,448 @@
1
+ import datetime
2
+ from typing import Optional, Any
3
+ from dataclasses import dataclass, field
4
+ from dataclasses_json import config, dataclass_json, Undefined
5
+ from plexflow.core.plex.hooks.plex_authorized import PlexAuthorizedHttpHook
6
+ from plexflow.core.plex.discover.comment import Comment, create_comment, get_comments
7
+
8
+
9
+ @dataclass_json(undefined=Undefined.EXCLUDE)
10
+ @dataclass
11
+ class MetadataItem:
12
+ id: Optional[str] = None
13
+ images: Optional[dict] = None
14
+ userState: Optional[dict] = None
15
+ title: Optional[str] = None
16
+ key: Optional[str] = None
17
+ type: Optional[str] = None
18
+ index: Optional[int] = None
19
+ publicPagesURL: Optional[str] = None
20
+ parent: Optional[dict] = None
21
+ grandparent: Optional[dict] = None
22
+ publishedAt: Optional[str] = None
23
+ leafCount: Optional[int] = None
24
+ year: Optional[int] = None
25
+ originallyAvailableAt: Optional[str] = None
26
+ childCount: Optional[int] = None
27
+ catchall: Optional[Any] = field(default_factory=dict)
28
+
29
+ def __post_init__(self):
30
+ if self.parent:
31
+ self.parent = MetadataItem(**self.parent)
32
+ if self.grandparent:
33
+ self.grandparent = MetadataItem(**self.grandparent)
34
+ if self.images:
35
+ self.images = ImageUrls(**self.images)
36
+ if self.userState:
37
+ self.userState = UserState(**self.userState)
38
+
39
+ @dataclass_json(undefined=Undefined.EXCLUDE)
40
+ @dataclass
41
+ class UserV2:
42
+ id: Optional[str] = None
43
+ username: Optional[str] = None
44
+ displayName: Optional[str] = None
45
+ avatar: Optional[str] = None
46
+ friendStatus: Optional[str] = None
47
+ isMuted: Optional[bool] = None
48
+ isBlocked: Optional[bool] = None
49
+ mutualFriends: Optional[dict] = None
50
+
51
+ def __post_init__(self):
52
+ if self.mutualFriends:
53
+ self.mutualFriends = MutualFriends(**self.mutualFriends)
54
+
55
+ @dataclass_json(undefined=Undefined.EXCLUDE)
56
+ @dataclass
57
+ class ImageUrls:
58
+ coverArt: Optional[str] = None
59
+ coverPoster: Optional[str] = None
60
+ thumbnail: Optional[str] = None
61
+ art: Optional[str] = None
62
+
63
+ @dataclass_json(undefined=Undefined.EXCLUDE)
64
+ @dataclass
65
+ class UserState:
66
+ viewCount: Optional[int] = None
67
+ viewedLeafCount: Optional[int] = None
68
+ watchlistedAt: Optional[str] = None
69
+
70
+ @dataclass_json(undefined=Undefined.EXCLUDE)
71
+ @dataclass
72
+ class MutualFriends:
73
+ count: Optional[int] = None
74
+ friends: Optional[list] = None
75
+
76
+ @dataclass_json(undefined=Undefined.EXCLUDE)
77
+ @dataclass
78
+ class ActivityData:
79
+ typename: Optional[str] = field(default= None, metadata=config(field_name="__typename"))
80
+ id: Optional[str] = None
81
+ date: Optional[str] = None
82
+ isPrimary: Optional[bool] = None
83
+ commentCount: Optional[int] = None
84
+ privacy: Optional[str] = None
85
+ isMuted: Optional[bool] = None
86
+ metadataItem: Optional[MetadataItem] = None
87
+ userV2: Optional[UserV2] = None
88
+ catchall: Optional[Any] = field(default_factory=dict)
89
+
90
+ from datetime import datetime
91
+ from typing import List
92
+ import requests
93
+
94
+ class Activity:
95
+ """
96
+ Represents an activity in Plex.
97
+
98
+ Attributes:
99
+ WATCH_HISTORY (str): Constant representing the watch history activity.
100
+ METADATA_MESSAGE (str): Constant representing the metadata message activity.
101
+ WATCHLIST (str): Constant representing the watchlist activity.
102
+ """
103
+
104
+ WATCH_HISTORY = "ActivityWatchHistory"
105
+ METADATA_MESSAGE = "ActivityMetadataMessage"
106
+ WATCHLIST = "ActivityWatchlist"
107
+
108
+ def __init__(self, data: dict):
109
+ """
110
+ Initializes a new instance of the Activity class.
111
+
112
+ Args:
113
+ data (dict): The data for the activity.
114
+ hook (PlexAuthorizedHttpHook): The Plex authorized HTTP hook.
115
+ """
116
+ self.data = ActivityData.from_dict(data)
117
+
118
+ def __str__(self):
119
+ """
120
+ Returns a string representation of the activity.
121
+
122
+ Returns:
123
+ str: The string representation of the activity.
124
+ """
125
+ return f"{self.data.typename} with ID {self.data.id}"
126
+
127
+ def __repr__(self):
128
+ """
129
+ Returns a string representation of the activity.
130
+
131
+ Returns:
132
+ str: The string representation of the activity.
133
+ """
134
+ return f"{self.data.typename} with ID {self.data.id}"
135
+
136
+ @property
137
+ def typename(self):
138
+ """
139
+ Gets the typename of the activity.
140
+
141
+ Returns:
142
+ str: The typename of the activity.
143
+ """
144
+ return self.data.typename
145
+
146
+ @property
147
+ def id(self):
148
+ """
149
+ Gets the ID of the activity.
150
+
151
+ Returns:
152
+ str: The ID of the activity.
153
+ """
154
+ return self.data.id
155
+
156
+ @property
157
+ def date(self):
158
+ """
159
+ Gets the date of the activity.
160
+
161
+ Returns:
162
+ datetime: The date of the activity.
163
+ """
164
+ return datetime.fromisoformat(self.data.date.replace("Z", "+00:00"))
165
+
166
+ @property
167
+ def isPrimary(self):
168
+ """
169
+ Gets a value indicating whether the activity is primary.
170
+
171
+ Returns:
172
+ bool: True if the activity is primary, False otherwise.
173
+ """
174
+ return self.data.isPrimary
175
+
176
+ @property
177
+ def commentCount(self):
178
+ """
179
+ Gets the comment count of the activity.
180
+
181
+ Returns:
182
+ int: The comment count of the activity.
183
+ """
184
+ return self.data.commentCount
185
+
186
+ @property
187
+ def privacy(self):
188
+ """
189
+ Gets the privacy of the activity.
190
+
191
+ Returns:
192
+ str: The privacy of the activity.
193
+ """
194
+ return self.data.privacy
195
+
196
+ @property
197
+ def isMuted(self):
198
+ """
199
+ Gets a value indicating whether the activity is muted.
200
+
201
+ Returns:
202
+ bool: True if the activity is muted, False otherwise.
203
+ """
204
+ return self.data.isMuted
205
+
206
+ @property
207
+ def metadataItem(self):
208
+ """
209
+ Gets the metadata item of the activity.
210
+
211
+ Returns:
212
+ str: The metadata item of the activity.
213
+ """
214
+ return self.data.metadataItem
215
+
216
+ @property
217
+ def userV2(self):
218
+ """
219
+ Gets the userV2 of the activity.
220
+
221
+ Returns:
222
+ str: The userV2 of the activity.
223
+ """
224
+ return self.data.userV2
225
+
226
+ @property
227
+ def comments(self) -> List[Comment]:
228
+ """
229
+ Gets the comments for the activity.
230
+
231
+ Returns:
232
+ list: The comments for the activity.
233
+ """
234
+ return get_comments(self.id)
235
+
236
+ def create_comment(self, message: str):
237
+ """
238
+ Creates a comment for the activity.
239
+
240
+ Args:
241
+ message (str): The message for the comment.
242
+ """
243
+ return create_comment(self.id, message)
244
+
245
+ def get_activities(total: int = 24) -> List[Activity]:
246
+ """
247
+ Retrieves a list of activities from the Plex server.
248
+
249
+ Args:
250
+ total (int, optional): The maximum number of activities to retrieve. Defaults to 24.
251
+
252
+ Returns:
253
+ list: A list of Activity objects representing the retrieved activities.
254
+
255
+ Raises:
256
+ HTTPError: If there is an error in the HTTP request.
257
+
258
+ Example:
259
+ >>> activities = get_activities(total=10)
260
+ >>> for activity in activities:
261
+ ... print(activity.metadataItem.title)
262
+ """
263
+ hook = PlexAuthorizedHttpHook(
264
+ method="POST", http_conn_id="plex_community", config_folder="config"
265
+ )
266
+
267
+ query = """
268
+ query GetActivityFeed($first: PaginationInt!, $after: String, $metadataID: ID, $types: [ActivityType!]!, $skipUserState: Boolean = false, $includeDescendants: Boolean = false, $skipWatchSession: Boolean = true) {
269
+ activityFeed(
270
+ first: $first
271
+ after: $after
272
+ metadataID: $metadataID
273
+ types: $types
274
+ includeDescendants: $includeDescendants
275
+ ) {
276
+ nodes {
277
+ __typename
278
+ id
279
+ date
280
+ isPrimary
281
+ commentCount
282
+ privacy
283
+ isMuted
284
+ metadataItem {
285
+ id
286
+ images {
287
+ coverArt
288
+ coverPoster
289
+ thumbnail
290
+ art
291
+ }
292
+ userState @skip(if: $skipUserState) {
293
+ viewCount
294
+ viewedLeafCount
295
+ watchlistedAt
296
+ }
297
+ title
298
+ key
299
+ type
300
+ index
301
+ publicPagesURL
302
+ parent {
303
+ index
304
+ title
305
+ publishedAt
306
+ key
307
+ type
308
+ images {
309
+ coverArt
310
+ coverPoster
311
+ thumbnail
312
+ art
313
+ }
314
+ userState @skip(if: $skipUserState) {
315
+ viewCount
316
+ viewedLeafCount
317
+ watchlistedAt
318
+ }
319
+ }
320
+ grandparent {
321
+ index
322
+ title
323
+ publishedAt
324
+ key
325
+ type
326
+ images {
327
+ coverArt
328
+ coverPoster
329
+ thumbnail
330
+ art
331
+ }
332
+ userState @skip(if: $skipUserState) {
333
+ viewCount
334
+ viewedLeafCount
335
+ watchlistedAt
336
+ }
337
+ }
338
+ publishedAt
339
+ leafCount
340
+ year
341
+ originallyAvailableAt
342
+ childCount
343
+ }
344
+ userV2 {
345
+ id
346
+ username
347
+ displayName
348
+ avatar
349
+ friendStatus
350
+ isMuted
351
+ isBlocked
352
+ mutualFriends {
353
+ count
354
+ friends {
355
+ avatar
356
+ displayName
357
+ id
358
+ username
359
+ }
360
+ }
361
+ }
362
+ ... on ActivityMetadataMessage {
363
+ message
364
+ otherRecipientsV2 {
365
+ id
366
+ username
367
+ displayName
368
+ avatar
369
+ friendStatus
370
+ isMuted
371
+ isBlocked
372
+ mutualFriends {
373
+ count
374
+ friends {
375
+ avatar
376
+ displayName
377
+ id
378
+ username
379
+ }
380
+ }
381
+ }
382
+ }
383
+ ... on ActivityMetadataReport {
384
+ message
385
+ otherRecipientsV2 {
386
+ id
387
+ username
388
+ displayName
389
+ avatar
390
+ friendStatus
391
+ isMuted
392
+ isBlocked
393
+ mutualFriends {
394
+ count
395
+ friends {
396
+ avatar
397
+ displayName
398
+ id
399
+ username
400
+ }
401
+ }
402
+ }
403
+ }
404
+ ... on ActivityRating {
405
+ rating
406
+ }
407
+ ... on ActivityPost {
408
+ message
409
+ }
410
+ ... on ActivityWatchHistory {
411
+ watchSession @skip(if: $skipWatchSession)
412
+ }
413
+ ... on ActivityWatchSession {
414
+ episodeCount
415
+ }
416
+ ... on ActivityWatchRating {
417
+ rating
418
+ }
419
+ }
420
+ pageInfo {
421
+ endCursor
422
+ hasNextPage
423
+ }
424
+ }
425
+ }
426
+ """
427
+
428
+ data = {
429
+ "query": query,
430
+ "variables": {
431
+ "first": total,
432
+ "types": ["METADATA_MESSAGE", "RATING", "WATCH_HISTORY", "WATCHLIST", "POST", "WATCH_SESSION", "WATCH_RATING"],
433
+ "includeDescendants": True,
434
+ "skipUserState": False,
435
+ "skipWatchSession": True
436
+ },
437
+ "operationName": "GetActivityFeed"
438
+ }
439
+
440
+ try:
441
+ response = hook.run(
442
+ endpoint="/api",
443
+ json=data,
444
+ )
445
+ response.raise_for_status()
446
+ return [Activity(node) for node in response.json()["data"]["activityFeed"]["nodes"]]
447
+ except requests.exceptions.HTTPError as e:
448
+ raise requests.HTTPError(f"HTTP request error: {e}")
@@ -0,0 +1,89 @@
1
+
2
+ from plexflow.core.plex.hooks.plex_authorized import PlexAuthorizedHttpHook
3
+ from dataclasses import dataclass
4
+ from dataclasses_json import dataclass_json
5
+ from dataclasses import dataclass
6
+ from dataclasses_json import dataclass_json
7
+ from typing import Optional
8
+
9
+ @dataclass_json
10
+ @dataclass
11
+ class User:
12
+ id: Optional[str]
13
+ avatar: Optional[str]
14
+ username: Optional[str]
15
+ displayName: Optional[str]
16
+ isBlocked: Optional[bool]
17
+ isMuted: Optional[bool]
18
+ isHidden: Optional[bool]
19
+
20
+ @dataclass_json
21
+ @dataclass
22
+ class Comment:
23
+ date: Optional[str]
24
+ id: Optional[str]
25
+ message: Optional[str]
26
+ user: Optional[User]
27
+
28
+ def get_comments(activity_id):
29
+ """
30
+ Retrieves comments for a specific activity.
31
+
32
+ Args:
33
+ activity_id (str): The ID of the activity to retrieve comments for.
34
+
35
+ Returns:
36
+ list: A list of Comment objects representing the comments for the activity.
37
+
38
+ Raises:
39
+ HTTPError: If there was an error while making the API request.
40
+
41
+ Example:
42
+ >>> comments = get_comments("12345")
43
+ >>> for comment in comments:
44
+ ... print(comment.message)
45
+ """
46
+ hook = PlexAuthorizedHttpHook(method="POST", http_conn_id="plex_community", config_folder="config")
47
+
48
+ data = {
49
+ "query": "query getActivityComments($id: ID!, $first: PaginationInt, $after: String, $last: PaginationInt, $before: String) { activityComments(first: $first after: $after id: $id last: $last before: $before) { nodes { date id message user { id avatar username displayName isBlocked isMuted isHidden } } pageInfo { endCursor hasNextPage hasPreviousPage startCursor } } }",
50
+ "variables": {"first": 50, "id": activity_id},
51
+ "operationName": "getActivityComments"
52
+ }
53
+
54
+ response = hook.run(
55
+ endpoint="/api",
56
+ json=data,
57
+ )
58
+
59
+ response.raise_for_status()
60
+
61
+ return [Comment.from_dict(node) for node in response.json()["data"]["activityComments"]["nodes"]]
62
+
63
+ def create_comment(activity_id, message):
64
+ """
65
+ Create a comment for a given activity.
66
+
67
+ Args:
68
+ activity_id (str): The ID of the activity to create the comment for.
69
+ message (str): The content of the comment.
70
+
71
+ Returns:
72
+ Comment: The created comment object.
73
+
74
+ Raises:
75
+ HTTPError: If the request to create the comment fails.
76
+
77
+ Example:
78
+ >>> create_comment("12345", "Great work!")
79
+ """
80
+ hook = PlexAuthorizedHttpHook(method="POST", http_conn_id="plex_community", config_folder="config")
81
+
82
+ data = {
83
+ "query": "mutation createComment($input: CreateCommentInput!) { createComment(input: $input) { date id message user { id avatar username displayName isHidden isMuted isBlocked } } }",
84
+ "variables": {"input": {"activity": activity_id, "message": message}},
85
+ "operationName": "createComment"
86
+ }
87
+
88
+ response = hook.run(endpoint="/api", json=data)
89
+ response.raise_for_status()
@@ -0,0 +1,11 @@
1
+ from typing import List
2
+ from plexflow.core.plex.hooks.plex_authorized import PlexAuthorizedHttpHook
3
+ from plexflow.core.plex.discover.activity import Activity, get_activities
4
+
5
+ class ActivityFeed:
6
+ def __init__(self):
7
+ pass
8
+
9
+ @property
10
+ def activities(self) -> List[Activity]:
11
+ return get_activities(total=100)
File without changes
@@ -0,0 +1,60 @@
1
+ from typing import Optional, Dict, Any
2
+ from plexflow.utils.hooks.http import UniversalHttpHook
3
+ import requests
4
+ from plexflow.core.plex.token.auto_token import PlexAutoToken
5
+
6
+ class PlexAuthorizedHttpHook(UniversalHttpHook):
7
+ """
8
+ A subclass of UniversalHttpHook that includes the X-Plex-Token as a query parameter.
9
+
10
+ When used with Airflow, connection details are fetched from Airflow Connections.
11
+ When used standalone, these details should be loaded from a YAML file named after the connection ID.
12
+
13
+ Args:
14
+ plex_token (str): The Plex token.
15
+ method (str, optional): The HTTP method. Defaults to 'GET'.
16
+ http_conn_id (str, optional): The Airflow connection ID or the name for the YAML file. Defaults to None.
17
+ config_folder (str, optional): The folder where the YAML configuration file is located. Defaults to None.
18
+
19
+ Attributes:
20
+ hook (HttpHook, optional): The Airflow HttpHook instance.
21
+ session (requests.Session, optional): The requests Session instance.
22
+ config (dict, optional): The configuration loaded from the YAML file.
23
+
24
+ Examples:
25
+ Using PlexAuthorizedHttpHook with Airflow:
26
+ hook = PlexAuthorizedHttpHook(plex_token='my_plex_token', method='GET', http_conn_id='my_http_connection')
27
+ response = hook.run('/api/v1/resource')
28
+
29
+ Using PlexAuthorizedHttpHook in standalone mode with a YAML configuration file:
30
+ hook = PlexAuthorizedHttpHook(plex_token='my_plex_token', method='POST', http_conn_id='my_http_connection', config_folder='/path/to/configs')
31
+ response = hook.run('/api/v1/resource', data={'key': 'value'})
32
+ """
33
+
34
+ def __init__(self, plex_token: str = None, method: str = 'GET', http_conn_id: Optional[str] = None, config_folder: Optional[str] = None):
35
+ super().__init__(method, http_conn_id, config_folder)
36
+ self.plex_token = plex_token
37
+
38
+ def run(self, endpoint: str, data: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, extra_options: Optional[Dict[str, Any]] = None, query_params: Optional[Dict[str, str]] = None, json: Any = None) -> requests.Response:
39
+ """
40
+ Makes an HTTP request.
41
+
42
+ Args:
43
+ endpoint (str): The endpoint for the HTTP request.
44
+ data (dict, optional): The data for the HTTP request. Defaults to None.
45
+ headers (dict, optional): The headers for the HTTP request. Defaults to None.
46
+ extra_options (dict, optional): Extra options for the HTTP request. Defaults to None.
47
+ query_params (dict, optional): The query parameters for the HTTP request. Defaults to None.
48
+
49
+ Returns:
50
+ The response from the HTTP request.
51
+ """
52
+ query_params = query_params or {}
53
+ query_params['X-Plex-Token'] = PlexAutoToken(plex_token=self.plex_token).get_token()
54
+
55
+ headers = headers or {}
56
+ headers["Accept"] = "application/json"
57
+
58
+ print(query_params)
59
+
60
+ return super().run(endpoint=endpoint, data=data, headers=headers, extra_options=extra_options, query_params=query_params, json=json)
@@ -0,0 +1,6 @@
1
+ from typing import Optional, Dict, Any
2
+ from plexflow.utils.hooks.postgresql import UniversalPostgresqlHook
3
+
4
+ class PlexflowDatabase(UniversalPostgresqlHook):
5
+ def __init__(self, postgres_conn_id: Optional[str] = None, config_folder: Optional[str] = None):
6
+ super().__init__(postgres_conn_id, config_folder)
File without changes