plexflow 0.0.64__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plexflow/__init__.py +0 -0
- plexflow/__main__.py +15 -0
- plexflow/core/.DS_Store +0 -0
- plexflow/core/__init__.py +0 -0
- plexflow/core/context/__init__.py +0 -0
- plexflow/core/context/metadata/__init__.py +0 -0
- plexflow/core/context/metadata/context.py +32 -0
- plexflow/core/context/metadata/tmdb/__init__.py +0 -0
- plexflow/core/context/metadata/tmdb/context.py +45 -0
- plexflow/core/context/partial_context.py +46 -0
- plexflow/core/context/partials/__init__.py +8 -0
- plexflow/core/context/partials/cache.py +16 -0
- plexflow/core/context/partials/context.py +12 -0
- plexflow/core/context/partials/ids.py +37 -0
- plexflow/core/context/partials/movie.py +115 -0
- plexflow/core/context/partials/tgx_batch.py +33 -0
- plexflow/core/context/partials/tgx_context.py +34 -0
- plexflow/core/context/partials/torrents.py +23 -0
- plexflow/core/context/partials/watchlist.py +35 -0
- plexflow/core/context/plexflow_context.py +29 -0
- plexflow/core/context/plexflow_property.py +36 -0
- plexflow/core/context/root/__init__.py +0 -0
- plexflow/core/context/root/context.py +25 -0
- plexflow/core/context/select/__init__.py +0 -0
- plexflow/core/context/select/context.py +45 -0
- plexflow/core/context/torrent/__init__.py +0 -0
- plexflow/core/context/torrent/context.py +43 -0
- plexflow/core/context/torrent/tpb/__init__.py +0 -0
- plexflow/core/context/torrent/tpb/context.py +45 -0
- plexflow/core/context/torrent/yts/__init__.py +0 -0
- plexflow/core/context/torrent/yts/context.py +45 -0
- plexflow/core/context/watchlist/__init__.py +0 -0
- plexflow/core/context/watchlist/context.py +46 -0
- plexflow/core/downloads/__init__.py +0 -0
- plexflow/core/downloads/candidates/__init__.py +0 -0
- plexflow/core/downloads/candidates/download_candidate.py +210 -0
- plexflow/core/downloads/candidates/filtered.py +51 -0
- plexflow/core/downloads/candidates/utils.py +39 -0
- plexflow/core/env/__init__.py +0 -0
- plexflow/core/env/env.py +31 -0
- plexflow/core/genai/__init__.py +0 -0
- plexflow/core/genai/bot.py +9 -0
- plexflow/core/genai/plexa.py +54 -0
- plexflow/core/genai/torrent/imdb_verify.py +65 -0
- plexflow/core/genai/torrent/movie.py +25 -0
- plexflow/core/genai/utils/__init__.py +0 -0
- plexflow/core/genai/utils/loader.py +5 -0
- plexflow/core/metadata/__init__.py +0 -0
- plexflow/core/metadata/auto/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_meta.py +40 -0
- plexflow/core/metadata/auto/auto_providers/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/auto/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/auto/episode.py +49 -0
- plexflow/core/metadata/auto/auto_providers/auto/item.py +55 -0
- plexflow/core/metadata/auto/auto_providers/auto/movie.py +13 -0
- plexflow/core/metadata/auto/auto_providers/auto/season.py +43 -0
- plexflow/core/metadata/auto/auto_providers/auto/show.py +26 -0
- plexflow/core/metadata/auto/auto_providers/imdb/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/imdb/movie.py +36 -0
- plexflow/core/metadata/auto/auto_providers/imdb/show.py +45 -0
- plexflow/core/metadata/auto/auto_providers/moviemeter/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/moviemeter/movie.py +40 -0
- plexflow/core/metadata/auto/auto_providers/plex/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/plex/movie.py +39 -0
- plexflow/core/metadata/auto/auto_providers/tmdb/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/tmdb/episode.py +30 -0
- plexflow/core/metadata/auto/auto_providers/tmdb/movie.py +36 -0
- plexflow/core/metadata/auto/auto_providers/tmdb/season.py +23 -0
- plexflow/core/metadata/auto/auto_providers/tmdb/show.py +41 -0
- plexflow/core/metadata/auto/auto_providers/tmdb.py +92 -0
- plexflow/core/metadata/auto/auto_providers/tvdb/__init__.py +0 -0
- plexflow/core/metadata/auto/auto_providers/tvdb/episode.py +28 -0
- plexflow/core/metadata/auto/auto_providers/tvdb/movie.py +36 -0
- plexflow/core/metadata/auto/auto_providers/tvdb/season.py +25 -0
- plexflow/core/metadata/auto/auto_providers/tvdb/show.py +41 -0
- plexflow/core/metadata/providers/__init__.py +0 -0
- plexflow/core/metadata/providers/imdb/__init__.py +0 -0
- plexflow/core/metadata/providers/imdb/datatypes.py +53 -0
- plexflow/core/metadata/providers/imdb/imdb.py +112 -0
- plexflow/core/metadata/providers/moviemeter/__init__.py +0 -0
- plexflow/core/metadata/providers/moviemeter/datatypes.py +111 -0
- plexflow/core/metadata/providers/moviemeter/moviemeter.py +42 -0
- plexflow/core/metadata/providers/plex/__init__.py +0 -0
- plexflow/core/metadata/providers/plex/datatypes.py +693 -0
- plexflow/core/metadata/providers/plex/plex.py +167 -0
- plexflow/core/metadata/providers/tmdb/__init__.py +0 -0
- plexflow/core/metadata/providers/tmdb/datatypes.py +460 -0
- plexflow/core/metadata/providers/tmdb/tmdb.py +85 -0
- plexflow/core/metadata/providers/tvdb/__init__.py +0 -0
- plexflow/core/metadata/providers/tvdb/datatypes.py +257 -0
- plexflow/core/metadata/providers/tvdb/tv_datatypes.py +554 -0
- plexflow/core/metadata/providers/tvdb/tvdb.py +65 -0
- plexflow/core/metadata/providers/universal/__init__.py +0 -0
- plexflow/core/metadata/providers/universal/movie.py +130 -0
- plexflow/core/metadata/providers/universal/old.py +192 -0
- plexflow/core/metadata/providers/universal/show.py +107 -0
- plexflow/core/plex/__init__.py +0 -0
- plexflow/core/plex/api/context/authorized.py +15 -0
- plexflow/core/plex/api/context/discover.py +14 -0
- plexflow/core/plex/api/context/library.py +14 -0
- plexflow/core/plex/discover/__init__.py +0 -0
- plexflow/core/plex/discover/activity.py +448 -0
- plexflow/core/plex/discover/comment.py +89 -0
- plexflow/core/plex/discover/feed.py +11 -0
- plexflow/core/plex/hooks/__init__.py +0 -0
- plexflow/core/plex/hooks/plex_authorized.py +60 -0
- plexflow/core/plex/hooks/plexflow_database.py +6 -0
- plexflow/core/plex/library/__init__.py +0 -0
- plexflow/core/plex/library/library.py +103 -0
- plexflow/core/plex/token/__init__.py +0 -0
- plexflow/core/plex/token/auto_token.py +91 -0
- plexflow/core/plex/utils/__init__.py +0 -0
- plexflow/core/plex/utils/paginated.py +39 -0
- plexflow/core/plex/watchlist/__init__.py +0 -0
- plexflow/core/plex/watchlist/datatypes.py +124 -0
- plexflow/core/plex/watchlist/watchlist.py +23 -0
- plexflow/core/storage/__init__.py +0 -0
- plexflow/core/storage/object/__init__.py +0 -0
- plexflow/core/storage/object/plexflow_storage.py +143 -0
- plexflow/core/storage/object/redis_storage.py +169 -0
- plexflow/core/subtitles/__init__.py +0 -0
- plexflow/core/subtitles/providers/__init__.py +0 -0
- plexflow/core/subtitles/providers/auto_subtitles.py +48 -0
- plexflow/core/subtitles/providers/oss/__init__.py +0 -0
- plexflow/core/subtitles/providers/oss/datatypes.py +104 -0
- plexflow/core/subtitles/providers/oss/download.py +48 -0
- plexflow/core/subtitles/providers/oss/old.py +144 -0
- plexflow/core/subtitles/providers/oss/oss.py +400 -0
- plexflow/core/subtitles/providers/oss/oss_subtitle.py +32 -0
- plexflow/core/subtitles/providers/oss/search.py +52 -0
- plexflow/core/subtitles/providers/oss/unlimited_oss.py +231 -0
- plexflow/core/subtitles/providers/oss/utils/__init__.py +0 -0
- plexflow/core/subtitles/providers/oss/utils/config.py +63 -0
- plexflow/core/subtitles/providers/oss/utils/download_client.py +22 -0
- plexflow/core/subtitles/providers/oss/utils/exceptions.py +35 -0
- plexflow/core/subtitles/providers/oss/utils/file_utils.py +83 -0
- plexflow/core/subtitles/providers/oss/utils/languages.py +78 -0
- plexflow/core/subtitles/providers/oss/utils/response_base.py +221 -0
- plexflow/core/subtitles/providers/oss/utils/responses.py +176 -0
- plexflow/core/subtitles/providers/oss/utils/srt.py +561 -0
- plexflow/core/subtitles/results/__init__.py +0 -0
- plexflow/core/subtitles/results/subtitle.py +170 -0
- plexflow/core/torrents/__init__.py +0 -0
- plexflow/core/torrents/analyzers/analyzed_torrent.py +143 -0
- plexflow/core/torrents/analyzers/analyzer.py +45 -0
- plexflow/core/torrents/analyzers/torrentquest/analyzer.py +47 -0
- plexflow/core/torrents/auto/auto_providers/auto/__init__.py +0 -0
- plexflow/core/torrents/auto/auto_providers/auto/torrent.py +64 -0
- plexflow/core/torrents/auto/auto_providers/tpb/torrent.py +62 -0
- plexflow/core/torrents/auto/auto_torrents.py +29 -0
- plexflow/core/torrents/providers/__init__.py +0 -0
- plexflow/core/torrents/providers/ext/__init__.py +0 -0
- plexflow/core/torrents/providers/ext/ext.py +18 -0
- plexflow/core/torrents/providers/ext/utils.py +64 -0
- plexflow/core/torrents/providers/extratorrent/__init__.py +0 -0
- plexflow/core/torrents/providers/extratorrent/extratorrent.py +21 -0
- plexflow/core/torrents/providers/extratorrent/utils.py +66 -0
- plexflow/core/torrents/providers/eztv/__init__.py +0 -0
- plexflow/core/torrents/providers/eztv/eztv.py +47 -0
- plexflow/core/torrents/providers/eztv/utils.py +83 -0
- plexflow/core/torrents/providers/rarbg2/__init__.py +0 -0
- plexflow/core/torrents/providers/rarbg2/rarbg2.py +19 -0
- plexflow/core/torrents/providers/rarbg2/utils.py +76 -0
- plexflow/core/torrents/providers/snowfl/__init__.py +0 -0
- plexflow/core/torrents/providers/snowfl/snowfl.py +36 -0
- plexflow/core/torrents/providers/snowfl/utils.py +59 -0
- plexflow/core/torrents/providers/tgx/__init__.py +0 -0
- plexflow/core/torrents/providers/tgx/context.py +50 -0
- plexflow/core/torrents/providers/tgx/dump.py +40 -0
- plexflow/core/torrents/providers/tgx/tgx.py +22 -0
- plexflow/core/torrents/providers/tgx/utils.py +61 -0
- plexflow/core/torrents/providers/therarbg/__init__.py +0 -0
- plexflow/core/torrents/providers/therarbg/therarbg.py +17 -0
- plexflow/core/torrents/providers/therarbg/utils.py +61 -0
- plexflow/core/torrents/providers/torrentquest/__init__.py +0 -0
- plexflow/core/torrents/providers/torrentquest/torrentquest.py +20 -0
- plexflow/core/torrents/providers/torrentquest/utils.py +70 -0
- plexflow/core/torrents/providers/tpb/__init__.py +0 -0
- plexflow/core/torrents/providers/tpb/tpb.py +17 -0
- plexflow/core/torrents/providers/tpb/utils.py +139 -0
- plexflow/core/torrents/providers/yts/__init__.py +0 -0
- plexflow/core/torrents/providers/yts/utils.py +57 -0
- plexflow/core/torrents/providers/yts/yts.py +31 -0
- plexflow/core/torrents/results/__init__.py +0 -0
- plexflow/core/torrents/results/torrent.py +165 -0
- plexflow/core/torrents/results/universal.py +220 -0
- plexflow/core/torrents/results/utils.py +15 -0
- plexflow/events/__init__.py +0 -0
- plexflow/events/download/__init__.py +0 -0
- plexflow/events/download/torrent_events.py +96 -0
- plexflow/events/publish/__init__.py +0 -0
- plexflow/events/publish/publish.py +34 -0
- plexflow/logging/__init__.py +0 -0
- plexflow/logging/log_setup.py +8 -0
- plexflow/spiders/quiet_logger.py +9 -0
- plexflow/spiders/tgx/pipelines/dump_json_pipeline.py +30 -0
- plexflow/spiders/tgx/pipelines/meta_pipeline.py +13 -0
- plexflow/spiders/tgx/pipelines/publish_pipeline.py +14 -0
- plexflow/spiders/tgx/pipelines/torrent_info_pipeline.py +12 -0
- plexflow/spiders/tgx/pipelines/validation_pipeline.py +17 -0
- plexflow/spiders/tgx/settings.py +36 -0
- plexflow/spiders/tgx/spider.py +72 -0
- plexflow/utils/__init__.py +0 -0
- plexflow/utils/antibot/human_like_requests.py +122 -0
- plexflow/utils/api/__init__.py +0 -0
- plexflow/utils/api/context/http.py +62 -0
- plexflow/utils/api/rest/__init__.py +0 -0
- plexflow/utils/api/rest/antibot_restful.py +68 -0
- plexflow/utils/api/rest/restful.py +49 -0
- plexflow/utils/captcha/__init__.py +0 -0
- plexflow/utils/captcha/bypass/__init__.py +0 -0
- plexflow/utils/captcha/bypass/decode_audio.py +34 -0
- plexflow/utils/download/__init__.py +0 -0
- plexflow/utils/download/gz.py +26 -0
- plexflow/utils/filesystem/__init__.py +0 -0
- plexflow/utils/filesystem/search.py +129 -0
- plexflow/utils/gmail/__init__.py +0 -0
- plexflow/utils/gmail/mails.py +116 -0
- plexflow/utils/hooks/__init__.py +0 -0
- plexflow/utils/hooks/http.py +84 -0
- plexflow/utils/hooks/postgresql.py +93 -0
- plexflow/utils/hooks/redis.py +112 -0
- plexflow/utils/image/storage.py +36 -0
- plexflow/utils/imdb/__init__.py +0 -0
- plexflow/utils/imdb/imdb_codes.py +107 -0
- plexflow/utils/pubsub/consume.py +82 -0
- plexflow/utils/pubsub/produce.py +25 -0
- plexflow/utils/retry/__init__.py +0 -0
- plexflow/utils/retry/utils.py +38 -0
- plexflow/utils/strings/__init__.py +0 -0
- plexflow/utils/strings/filesize.py +55 -0
- plexflow/utils/strings/language.py +14 -0
- plexflow/utils/subtitle/search.py +76 -0
- plexflow/utils/tasks/decorators.py +78 -0
- plexflow/utils/tasks/k8s/task.py +70 -0
- plexflow/utils/thread_safe/safe_list.py +54 -0
- plexflow/utils/thread_safe/safe_set.py +69 -0
- plexflow/utils/torrent/__init__.py +0 -0
- plexflow/utils/torrent/analyze.py +118 -0
- plexflow/utils/torrent/extract/common.py +37 -0
- plexflow/utils/torrent/extract/ext.py +2391 -0
- plexflow/utils/torrent/extract/extratorrent.py +56 -0
- plexflow/utils/torrent/extract/kat.py +1581 -0
- plexflow/utils/torrent/extract/tgx.py +96 -0
- plexflow/utils/torrent/extract/therarbg.py +170 -0
- plexflow/utils/torrent/extract/torrentquest.py +171 -0
- plexflow/utils/torrent/files.py +36 -0
- plexflow/utils/torrent/hash.py +90 -0
- plexflow/utils/transcribe/__init__.py +0 -0
- plexflow/utils/transcribe/speech2text.py +40 -0
- plexflow/utils/video/__init__.py +0 -0
- plexflow/utils/video/subtitle.py +73 -0
- plexflow-0.0.64.dist-info/METADATA +71 -0
- plexflow-0.0.64.dist-info/RECORD +256 -0
- plexflow-0.0.64.dist-info/WHEEL +4 -0
- plexflow-0.0.64.dist-info/entry_points.txt +24 -0
@@ -0,0 +1,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
|