webscout 6.4__py3-none-any.whl → 6.5__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.

Potentially problematic release.


This version of webscout might be problematic. Click here for more details.

Files changed (69) hide show
  1. webscout/AIutel.py +7 -54
  2. webscout/DWEBS.py +48 -26
  3. webscout/{YTdownloader.py → Extra/YTToolkit/YTdownloader.py} +990 -1103
  4. webscout/Extra/YTToolkit/__init__.py +3 -0
  5. webscout/{transcriber.py → Extra/YTToolkit/transcriber.py} +1 -1
  6. webscout/Extra/YTToolkit/ytapi/__init__.py +6 -0
  7. webscout/Extra/YTToolkit/ytapi/channel.py +307 -0
  8. webscout/Extra/YTToolkit/ytapi/errors.py +13 -0
  9. webscout/Extra/YTToolkit/ytapi/extras.py +45 -0
  10. webscout/Extra/YTToolkit/ytapi/https.py +88 -0
  11. webscout/Extra/YTToolkit/ytapi/patterns.py +61 -0
  12. webscout/Extra/YTToolkit/ytapi/playlist.py +59 -0
  13. webscout/Extra/YTToolkit/ytapi/pool.py +8 -0
  14. webscout/Extra/YTToolkit/ytapi/query.py +37 -0
  15. webscout/Extra/YTToolkit/ytapi/stream.py +60 -0
  16. webscout/Extra/YTToolkit/ytapi/utils.py +62 -0
  17. webscout/Extra/YTToolkit/ytapi/video.py +102 -0
  18. webscout/Extra/__init__.py +2 -1
  19. webscout/Extra/autocoder/rawdog.py +679 -680
  20. webscout/Extra/gguf.py +441 -441
  21. webscout/Extra/markdownlite/__init__.py +862 -0
  22. webscout/Extra/weather_ascii.py +2 -2
  23. webscout/Provider/PI.py +292 -221
  24. webscout/Provider/Perplexity.py +6 -14
  25. webscout/Provider/Reka.py +0 -1
  26. webscout/Provider/TTS/__init__.py +5 -1
  27. webscout/Provider/TTS/deepgram.py +183 -0
  28. webscout/Provider/TTS/elevenlabs.py +137 -0
  29. webscout/Provider/TTS/gesserit.py +151 -0
  30. webscout/Provider/TTS/murfai.py +139 -0
  31. webscout/Provider/TTS/parler.py +134 -107
  32. webscout/Provider/TTS/streamElements.py +360 -275
  33. webscout/Provider/TTS/utils.py +280 -0
  34. webscout/Provider/TTS/voicepod.py +116 -116
  35. webscout/Provider/__init__.py +146 -146
  36. webscout/Provider/meta.py +794 -779
  37. webscout/Provider/typegpt.py +1 -2
  38. webscout/__init__.py +24 -28
  39. webscout/litprinter/__init__.py +831 -830
  40. webscout/optimizers.py +269 -269
  41. webscout/prompt_manager.py +279 -279
  42. webscout/scout/__init__.py +11 -0
  43. webscout/scout/core.py +884 -0
  44. webscout/scout/element.py +459 -0
  45. webscout/scout/parsers/__init__.py +69 -0
  46. webscout/scout/parsers/html5lib_parser.py +172 -0
  47. webscout/scout/parsers/html_parser.py +236 -0
  48. webscout/scout/parsers/lxml_parser.py +178 -0
  49. webscout/scout/utils.py +38 -0
  50. webscout/update_checker.py +125 -125
  51. webscout/version.py +1 -1
  52. webscout/zeroart/__init__.py +55 -0
  53. webscout/zeroart/base.py +61 -0
  54. webscout/zeroart/effects.py +99 -0
  55. webscout/zeroart/fonts.py +816 -0
  56. webscout/zerodir/__init__.py +225 -0
  57. {webscout-6.4.dist-info → webscout-6.5.dist-info}/METADATA +12 -68
  58. {webscout-6.4.dist-info → webscout-6.5.dist-info}/RECORD +62 -37
  59. webscout/Agents/Onlinesearcher.py +0 -182
  60. webscout/Agents/__init__.py +0 -2
  61. webscout/Agents/functioncall.py +0 -248
  62. webscout/Bing_search.py +0 -251
  63. webscout/gpt4free.py +0 -666
  64. webscout/requestsHTMLfix.py +0 -775
  65. webscout/webai.py +0 -2590
  66. {webscout-6.4.dist-info → webscout-6.5.dist-info}/LICENSE.md +0 -0
  67. {webscout-6.4.dist-info → webscout-6.5.dist-info}/WHEEL +0 -0
  68. {webscout-6.4.dist-info → webscout-6.5.dist-info}/entry_points.txt +0 -0
  69. {webscout-6.4.dist-info → webscout-6.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,3 @@
1
+ from .YTdownloader import *
2
+ from .transcriber import *
3
+ from .ytapi import *
@@ -1,6 +1,6 @@
1
1
  """Wassup fam! 🔥 This module is your go-to for getting those YouTube transcripts!
2
2
 
3
- >>> from webscout.transcriber import YTTranscriber
3
+ >>> from webscout import YTTranscriber
4
4
  >>> transcript = YTTranscriber.get_transcript('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
5
5
  >>> print(transcript)
6
6
  {'text': 'Never gonna give you up', 'start': 0.0, 'duration': 4.5}
@@ -0,0 +1,6 @@
1
+ from .errors import *
2
+ from .video import Video
3
+ from .query import Search
4
+ from .extras import Extras
5
+ from .channel import Channel
6
+ from .playlist import Playlist
@@ -0,0 +1,307 @@
1
+ import json
2
+ import re
3
+
4
+ from .https import (
5
+ channel_about,
6
+ streams_data,
7
+ uploads_data,
8
+ channel_playlists,
9
+ upcoming_videos
10
+ )
11
+ from .video import Video
12
+ from .pool import collect
13
+ from .utils import dup_filter
14
+ from urllib.parse import unquote
15
+ from typing import List, Optional, Dict
16
+ from .patterns import _ChannelPatterns as Patterns
17
+
18
+
19
+ class Channel:
20
+
21
+ _HEAD = 'https://www.youtube.com/channel/'
22
+ _CUSTOM = 'https://www.youtube.com/c/'
23
+ _USER = 'https://www.youtube.com/'
24
+
25
+ def __init__(self, channel_id: str):
26
+ """
27
+ Represents a YouTube channel
28
+
29
+ Parameters
30
+ ----------
31
+ channel_id : str
32
+ The id or url or custom url or user id of the channel
33
+ """
34
+ pattern = re.compile("UC(.+)|c/(.+)|@(.+)")
35
+ results = pattern.findall(channel_id)
36
+ if not results:
37
+ self._usable_id = channel_id
38
+ self._target_url = self._CUSTOM + channel_id
39
+ elif results[0][0]:
40
+ self._usable_id = results[0][0]
41
+ self._target_url = self._HEAD + 'UC' + results[0][0]
42
+ elif results[0][1]:
43
+ self._usable_id = results[0][1]
44
+ self._target_url = self._CUSTOM + results[0][1]
45
+ elif results[0][2]:
46
+ self._usable_id = results[0][2]
47
+ self._target_url = self._USER + '@' + results[0][2]
48
+ self.id = None
49
+ self.name = None
50
+ self.subscribers = None
51
+ self.views = None
52
+ self.country = None
53
+ self.custom_url = None
54
+ self.avatar = None
55
+ self.banner = None
56
+ self.url = None
57
+ self.description = None
58
+ self.socials = None
59
+ self.__meta = None
60
+ self._about_page = channel_about(self._target_url)
61
+ self.__populate()
62
+
63
+ def __populate(self):
64
+ self.__meta = self.__prepare_metadata()
65
+ for k, v in self.__meta.items():
66
+ setattr(self, k, v)
67
+
68
+ def __repr__(self):
69
+ return f'<Channel `{self._target_url}`>'
70
+
71
+ def __prepare_metadata(self) -> Optional[Dict[str, any]]:
72
+ """
73
+ Returns channel metadata in a dict format
74
+
75
+ Returns
76
+ -------
77
+ Dict
78
+ Channel metadata containing the following keys:
79
+ id, name, subscribers, views, country, custom_url, avatar, banner, url, description, socials
80
+ """
81
+ patterns = [
82
+ Patterns.name,
83
+ Patterns.avatar,
84
+ Patterns.banner,
85
+ Patterns.verified,
86
+ Patterns.socials
87
+ ]
88
+ extracted = collect(lambda x: x.findall(self._about_page) or None, patterns)
89
+ name, avatar, banner, verified, socials = [e[0] if e else None for e in extracted]
90
+
91
+ # Add robust error handling for info extraction
92
+ info_pattern = re.compile("\\[{\"aboutChannelRenderer\":(.*?)],")
93
+ info_match = info_pattern.search(self._about_page)
94
+
95
+ if not info_match:
96
+ # Fallback metadata for search results or incomplete channel data
97
+ return {
98
+ "id": self._usable_id,
99
+ "name": name,
100
+ "url": self._target_url,
101
+ "description": None,
102
+ "country": None,
103
+ "custom_url": None,
104
+ "subscribers": None,
105
+ "views": None,
106
+ "created_at": None,
107
+ "video_count": None,
108
+ "avatar": avatar,
109
+ "banner": banner,
110
+ "verified": bool(verified),
111
+ "socials": unquote(socials) if socials is not None else None
112
+ }
113
+
114
+ try:
115
+ info_str = info_match.group(1) + "]}}}}"
116
+ info = json.loads(info_str)["metadata"]["aboutChannelViewModel"]
117
+
118
+ return {
119
+ "id": info.get("channelId", self._usable_id),
120
+ "name": name,
121
+ "url": "https://www.youtube.com/channel/" + info.get("channelId", self._usable_id),
122
+ "description": info.get("description"),
123
+ "country": info.get("country"),
124
+ "custom_url": info.get("canonicalChannelUrl"),
125
+ "subscribers": info.get("subscriberCountText", "").split(' ')[0] if "subscriberCountText" in info else None,
126
+ "views": info.get("viewCountText", "").replace(' views', '') if "viewCountText" in info else None,
127
+ "created_at": info.get("joinedDateText", {}).get("content", "").replace('Joined ', '') if "joinedDateText" in info else None,
128
+ "video_count": info.get("videoCountText", "").split(' ')[0] if "videoCountText" in info else None,
129
+ "avatar": avatar,
130
+ "banner": banner,
131
+ "verified": bool(verified),
132
+ "socials": unquote(socials) if socials is not None else None
133
+ }
134
+ except (KeyError, json.JSONDecodeError):
135
+ # Fallback if JSON parsing fails
136
+ return {
137
+ "id": self._usable_id,
138
+ "name": name,
139
+ "url": self._target_url,
140
+ "description": None,
141
+ "country": None,
142
+ "custom_url": None,
143
+ "subscribers": None,
144
+ "views": None,
145
+ "created_at": None,
146
+ "video_count": None,
147
+ "avatar": avatar,
148
+ "banner": banner,
149
+ "verified": bool(verified),
150
+ "socials": unquote(socials) if socials is not None else None
151
+ }
152
+
153
+ @property
154
+ def metadata(self) -> Optional[Dict[str, any]]:
155
+ """
156
+ Returns channel metadata in a dict format
157
+
158
+ Returns
159
+ -------
160
+ Dict
161
+ Channel metadata containing the following keys:
162
+ id, name, subscribers, views, country, custom_url, avatar, banner, url, description, socials etc.
163
+ """
164
+ return self.__meta
165
+
166
+ @property
167
+ def live(self) -> bool:
168
+ """
169
+ Checks if the channel is live
170
+
171
+ Returns
172
+ -------
173
+ bool
174
+ True if the channel is live
175
+ """
176
+ return bool(self.current_streams)
177
+
178
+ @property
179
+ def streaming_now(self) -> Optional[str]:
180
+ """
181
+ Fetches the id of currently streaming video
182
+
183
+ Returns
184
+ -------
185
+ str | None
186
+ The id of the currently streaming video or None
187
+ """
188
+ streams = self.current_streams
189
+ return streams[0] if streams else None
190
+
191
+ @property
192
+ def current_streams(self) -> Optional[List[str]]:
193
+ """
194
+ Fetches the ids of all ongoing streams
195
+
196
+ Returns
197
+ -------
198
+ List[str] | None
199
+ The ids of all ongoing streams or None
200
+ """
201
+ raw = streams_data(self._target_url)
202
+ filtered_ids = dup_filter(Patterns.stream_ids.findall(raw))
203
+ if not filtered_ids:
204
+ return None
205
+ return [id_ for id_ in filtered_ids if f"vi/{id_}/hqdefault_live.jpg" in raw]
206
+
207
+ @property
208
+ def old_streams(self) -> Optional[List[str]]:
209
+ """
210
+ Fetches the ids of all old or completed streams
211
+
212
+ Returns
213
+ -------
214
+ List[str] | None
215
+ The ids of all old or completed streams or None
216
+ """
217
+ raw = streams_data(self._target_url)
218
+ filtered_ids = dup_filter(Patterns.stream_ids.findall(raw))
219
+ if not filtered_ids:
220
+ return None
221
+ return [id_ for id_ in filtered_ids if f"vi/{id_}/hqdefault_live.jpg" not in raw]
222
+
223
+ @property
224
+ def last_streamed(self) -> Optional[str]:
225
+ """
226
+ Fetches the id of the last completed livestream
227
+
228
+ Returns
229
+ -------
230
+ str | None
231
+ The id of the last livestreamed video or None
232
+ """
233
+ ids = self.old_streams
234
+ return ids[0] if ids else None
235
+
236
+ def uploads(self, limit: int = 20) -> Optional[List[str]]:
237
+ """
238
+ Fetches the ids of all uploaded videos
239
+
240
+ Parameters
241
+ ----------
242
+ limit : int
243
+ The number of videos to fetch, defaults to 20
244
+
245
+ Returns
246
+ -------
247
+ List[str] | None
248
+ The ids of uploaded videos or None
249
+ """
250
+ return dup_filter(Patterns.upload_ids.findall(uploads_data(self._target_url)), limit)
251
+
252
+ @property
253
+ def last_uploaded(self) -> Optional[str]:
254
+ """
255
+ Fetches the id of the last uploaded video
256
+
257
+ Returns
258
+ -------
259
+ str | None
260
+ The id of the last uploaded video or None
261
+ """
262
+ ids = self.uploads()
263
+ return ids[0] if ids else None
264
+
265
+ @property
266
+ def upcoming(self) -> Optional[Video]:
267
+ """
268
+ Fetches the upcoming video
269
+
270
+ Returns
271
+ -------
272
+ Video | None
273
+ The upcoming video or None
274
+ """
275
+ raw = upcoming_videos(self._target_url)
276
+ if not Patterns.upcoming_check.search(raw):
277
+ return None
278
+ upcoming = Patterns.upcoming.findall(raw)
279
+ return Video(upcoming[0]) if upcoming else None
280
+
281
+ @property
282
+ def upcomings(self) -> Optional[List[str]]:
283
+ """
284
+ Fetches the upcoming videos
285
+
286
+ Returns
287
+ -------
288
+ List[str] | None
289
+ The ids of upcoming videos or None
290
+ """
291
+ raw = upcoming_videos(self._target_url)
292
+ if not Patterns.upcoming_check.search(raw):
293
+ return None
294
+ video_ids = Patterns.upcoming.findall(raw)
295
+ return video_ids
296
+
297
+ @property
298
+ def playlists(self) -> Optional[List[str]]:
299
+ """
300
+ Fetches the ids of all playlists
301
+
302
+ Returns
303
+ -------
304
+ List[str] | None
305
+ The ids of all playlists or None
306
+ """
307
+ return dup_filter(Patterns.playlists.findall(channel_playlists(self._target_url)))
@@ -0,0 +1,13 @@
1
+ class TooManyRequests(Exception):
2
+ def __init__(self, message):
3
+ self.message = message
4
+
5
+
6
+ class InvalidURL(Exception):
7
+ def __init__(self, message):
8
+ self.message = message
9
+
10
+
11
+ class RequestError(Exception):
12
+ def __init__(self, message):
13
+ self.message = message
@@ -0,0 +1,45 @@
1
+ from .https import (
2
+ trending_videos,
3
+ trending_songs,
4
+ trending_games,
5
+ trending_feeds,
6
+ trending_streams,
7
+ _get_trending_learning_videos,
8
+ trending_sports
9
+ )
10
+ from .utils import dup_filter
11
+ from .patterns import _ExtraPatterns as Patterns
12
+ from typing import Optional, List
13
+
14
+
15
+ class Extras:
16
+
17
+ @staticmethod
18
+ def trending_videos() -> Optional[List[str]]:
19
+ data = Patterns.video_id.findall(trending_videos())
20
+ return dup_filter(data) if data else None
21
+
22
+ @staticmethod
23
+ def music_videos() -> Optional[List[str]]:
24
+ data = Patterns.video_id.findall(trending_songs())
25
+ return dup_filter(data) if data else None
26
+
27
+ @staticmethod
28
+ def gaming_videos() -> Optional[List[str]]:
29
+ return dup_filter(Patterns.video_id.findall(trending_games()))
30
+
31
+ @staticmethod
32
+ def news_videos() -> Optional[List[str]]:
33
+ return dup_filter(Patterns.video_id.findall(trending_feeds()))
34
+
35
+ @staticmethod
36
+ def live_videos() -> Optional[List[str]]:
37
+ return dup_filter(Patterns.video_id.findall(trending_streams()))
38
+
39
+ @staticmethod
40
+ def educational_videos() -> Optional[List[str]]:
41
+ return dup_filter(Patterns.video_id.findall(_get_trending_learning_videos()))
42
+
43
+ @staticmethod
44
+ def sport_videos() -> Optional[List[str]]:
45
+ return dup_filter(Patterns.video_id.findall(trending_sports()))
@@ -0,0 +1,88 @@
1
+ from urllib.parse import quote
2
+ from .utils import request
3
+
4
+
5
+ def channel_about(head: str) -> str:
6
+ return request(head + '/about')
7
+
8
+
9
+ def video_count(channel_id: str) -> str:
10
+ head = 'https://www.youtube.com/results?search_query='
11
+ tail = '&sp=EgIQAg%253D%253D'
12
+ return request(head + channel_id + tail)
13
+
14
+
15
+ def uploads_data(head: str) -> str:
16
+ url = head + '/videos'
17
+ return request(url)
18
+
19
+
20
+ def streams_data(head: str) -> str:
21
+ url = head + '/streams'
22
+ return request(url)
23
+
24
+
25
+ def channel_playlists(head: str) -> str:
26
+ url = head + '/playlists'
27
+ return request(url)
28
+
29
+
30
+ def upcoming_videos(head: str) -> str:
31
+ url = head + '/videos?view=2&live_view=502'
32
+ return request(url)
33
+
34
+
35
+ def video_data(video_id: str) -> str:
36
+ url = f'https://www.youtube.com/watch?v={video_id}'
37
+ return request(url)
38
+
39
+
40
+ def playlist_data(playlist_id: str) -> str:
41
+ url = 'https://www.youtube.com/playlist?list=' + playlist_id
42
+ return request(url)
43
+
44
+
45
+ def trending_videos() -> str:
46
+ return request('https://www.youtube.com/feed/trending')
47
+
48
+
49
+ def trending_songs() -> str:
50
+ return request('https://www.youtube.com/feed/music')
51
+
52
+
53
+ def trending_games() -> str:
54
+ return request('https://www.youtube.com/gaming')
55
+
56
+
57
+ def trending_feeds() -> str:
58
+ return request('https://www.youtube.com/news')
59
+
60
+
61
+ def trending_streams() -> str:
62
+ return request('https://www.youtube.com/live')
63
+
64
+
65
+ def _get_trending_learning_videos() -> str:
66
+ return request('https://www.youtube.com/learning')
67
+
68
+
69
+ def trending_sports() -> str:
70
+ return request('https://www.youtube.com/sports')
71
+
72
+
73
+ def find_videos(query: str) -> str:
74
+ head = 'https://www.youtube.com/results?search_query='
75
+ tail = '&sp=EgIQAQ%253D%253D'
76
+ return request(head + quote(query) + tail)
77
+
78
+
79
+ def find_channels(query: str) -> str:
80
+ head = 'https://www.youtube.com/results?search_query='
81
+ tail = '&sp=EgIQAg%253D%253D'
82
+ return request(head + quote(query) + tail)
83
+
84
+
85
+ def find_playlists(query: str) -> str:
86
+ head = 'https://www.youtube.com/results?search_query='
87
+ tail = '&sp=EgIQAw%253D%253D'
88
+ return request(head + quote(query) + tail)
@@ -0,0 +1,61 @@
1
+ import re
2
+
3
+
4
+ class _ChannelPatterns:
5
+ name = re.compile('channelMetadataRenderer\":{\"title\":\"(.*?)\"')
6
+ id = re.compile('channelId\":\"(.*?)\"')
7
+ verified = re.compile('"label":"Verified"')
8
+ check_live = re.compile('{"text":"LIVE"}')
9
+ live = re.compile("thumbnailOverlays\":\[(.*?)]")
10
+ video_id = re.compile('videoId\":\"(.*?)\"')
11
+ uploads = re.compile("gridVideoRenderer\":{\"videoId\":\"(.*?)\"")
12
+ subscribers = re.compile("\"subscriberCountText\":{\"accessibility\":(.*?),")
13
+ views = re.compile("viewCountText\":{\"simpleText\":\"(.*?)\"}")
14
+ creation = re.compile("{\"text\":\"Joined \"},{\"text\":\"(.*?)\"}")
15
+ country = re.compile("country\":{\"simpleText\":\"(.*?)\"}")
16
+ custom_url = re.compile("canonicalChannelUrl\":\"(.*?)\"")
17
+ description = re.compile("{\"description\":{\"simpleText\":\"(.*?)\"}")
18
+ avatar = re.compile("height\":88},{\"url\":\"(.*?)\"")
19
+ banner = re.compile("width\":1280,\"height\":351},{\"url\":\"(.*?)\"")
20
+ playlists = re.compile("{\"url\":\"/playlist\\?list=(.*?)\"")
21
+ video_count = re.compile("videoCountText\":{\"runs\":\[{\"text\":(.*?)}")
22
+ socials = re.compile("q=https%3A%2F%2F(.*?)\"")
23
+ upload_ids = re.compile("videoId\":\"(.*?)\"")
24
+ stream_ids = re.compile("videoId\":\"(.*?)\"")
25
+ upload_chunk = re.compile("gridVideoRenderer\":{(.*?)\"navigationEndpoint")
26
+ upload_chunk_fl_1 = re.compile("simpleText\":\"Streamed")
27
+ upload_chunk_fl_2 = re.compile("default_live.")
28
+ upcoming_check = re.compile("\"title\":\"Upcoming live streams\"")
29
+ upcoming = re.compile("gridVideoRenderer\":{\"videoId\":\"(.*?)\"")
30
+
31
+
32
+ class _VideoPatterns:
33
+ video_id = re.compile('videoId\":\"(.*?)\"')
34
+ title = re.compile("title\":\"(.*?)\"")
35
+ duration = re.compile("approxDurationMs\":\"(.*?)\"")
36
+ upload_date = re.compile("uploadDate\":\"(.*?)\"")
37
+ author_id = re.compile("channelIds\":\[\"(.*?)\"")
38
+ description = re.compile("shortDescription\":\"(.*)\",\"isCrawlable")
39
+ tags = re.compile("<meta name=\"keywords\" content=\"(.*?)\">")
40
+ is_streamed = re.compile("simpleText\":\"Streamed live")
41
+ is_premiered = re.compile("dateText\":{\"simpleText\":\"Premiered")
42
+ views = re.compile("videoViewCountRenderer\":{\"viewCount\":{\"simpleText\":\"(.*?)\"")
43
+ likes = re.compile("toggledText\":{\"accessibility\":{\"accessibilityData\":{\"label\":\"(.*?) ")
44
+ thumbnail = re.compile("playerMicroformatRenderer\":{\"thumbnail\":{\"thumbnails\":\[{\"url\":\"(.*?)\"")
45
+
46
+
47
+ class _PlaylistPatterns:
48
+ name = re.compile("{\"title\":\"(.*?)\"")
49
+ video_count = re.compile("stats\":\[{\"runs\":\[{\"text\":\"(.*?)\"")
50
+ video_id = re.compile("videoId\":\"(.*?)\"")
51
+ thumbnail = re.compile("og:image\" content=\"(.*?)\?")
52
+
53
+
54
+ class _ExtraPatterns:
55
+ video_id = re.compile("videoId\":\"(.*?)\"")
56
+
57
+
58
+ class _QueryPatterns:
59
+ channel_id = re.compile("channelId\":\"(.*?)\"")
60
+ video_id = re.compile("videoId\":\"(.*?)\"")
61
+ playlist_id = re.compile("playlistId\":\"(.*?)\"")
@@ -0,0 +1,59 @@
1
+ import re
2
+ from typing import Dict, Any
3
+
4
+ from .pool import collect
5
+ from .utils import dup_filter
6
+ from .https import playlist_data
7
+ from .patterns import _PlaylistPatterns as Patterns
8
+
9
+
10
+ class Playlist:
11
+
12
+ def __init__(self, playlist_id: str):
13
+ """
14
+ Represents a YouTube Playlist
15
+
16
+ Parameters
17
+ ----------
18
+ playlist_id : str
19
+ The id or url of the playlist
20
+ """
21
+ pattern = re.compile('=(.+?)$|^PL(.+?)$')
22
+ match = pattern.search(playlist_id)
23
+ if not match:
24
+ raise ValueError(f'Invalid playlist id: {playlist_id}')
25
+ if match.group(1):
26
+ self.id = match.group(1)
27
+ elif match.group(2):
28
+ self.id = 'PL' + match.group(2)
29
+ self._playlist_data = playlist_data(self.id)
30
+
31
+ def __repr__(self):
32
+ return f'<Playlist {self.id}>'
33
+
34
+ @property
35
+ def metadata(self) -> Dict[str, Any]:
36
+ """
37
+ Fetches playlist metadata in a dict format
38
+
39
+ Returns
40
+ -------
41
+ Dict
42
+ Playlist metadata in a dict format containing keys: id, url, name, video_count, thumbnail,
43
+ """
44
+ patterns = [
45
+ Patterns.name,
46
+ Patterns.video_count,
47
+ Patterns.thumbnail,
48
+ Patterns.video_id,
49
+ ]
50
+ ext = collect(lambda x: x.findall(self._playlist_data) or None, patterns)
51
+ data = [e[0] if e else None for e in ext]
52
+ return {
53
+ 'id': self.id,
54
+ 'url': 'https://www.youtube.com/playlist?list=' + self.id,
55
+ 'name': data[0] if data else None,
56
+ 'video_count': data[1] if data else None,
57
+ 'thumbnail': data[2] if data else None,
58
+ 'videos': dup_filter(ext[3])
59
+ }
@@ -0,0 +1,8 @@
1
+ import concurrent.futures
2
+ from typing import Callable, List, Any
3
+
4
+
5
+ def collect(func: Callable, args: List[Any]) -> List[Any]:
6
+ max_workers = len(args) or 1
7
+ with concurrent.futures.ThreadPoolExecutor(max_workers) as exe:
8
+ return list(exe.map(func, args))
@@ -0,0 +1,37 @@
1
+ from .utils import dup_filter
2
+ from .video import Video
3
+ from .channel import Channel
4
+ from .playlist import Playlist
5
+ from .patterns import _QueryPatterns as Patterns
6
+ from typing import Optional, Dict, Any, List
7
+ from .https import find_videos, find_channels, find_playlists
8
+
9
+
10
+ class Search:
11
+
12
+ @staticmethod
13
+ def video(keywords: str) -> Optional[Video]:
14
+ video_ids = Patterns.video_id.findall(find_videos(keywords))
15
+ return Video(video_ids[0]) if video_ids else None
16
+
17
+ @staticmethod
18
+ def channel(keywords: str) -> Optional[Channel]:
19
+ channel_ids = Patterns.channel_id.findall(find_channels(keywords))
20
+ return Channel(channel_ids[0]) if channel_ids else None
21
+
22
+ @staticmethod
23
+ def playlist(keywords: str) -> Optional[Playlist]:
24
+ playlist_ids = Patterns.playlist_id.findall(find_playlists(keywords))
25
+ return Playlist(playlist_ids[0]) if playlist_ids else None
26
+
27
+ @staticmethod
28
+ def videos(keywords: str, limit: int = 20) -> Optional[List[str]]:
29
+ return dup_filter(Patterns.video_id.findall(find_videos(keywords)), limit)
30
+
31
+ @staticmethod
32
+ def channels(keywords: str, limit: int = 20) -> Optional[List[str]]:
33
+ return dup_filter(Patterns.channel_id.findall(find_channels(keywords)), limit)
34
+
35
+ @staticmethod
36
+ def playlists(keywords: str, limit: int = 20) -> Optional[List[str]]:
37
+ return dup_filter(Patterns.playlist_id.findall(find_playlists(keywords)), limit)