webscout 8.3.6__py3-none-any.whl → 2025.10.11__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.
- webscout/AIauto.py +250 -250
- webscout/AIbase.py +379 -379
- webscout/AIutel.py +60 -58
- webscout/Bard.py +1012 -1012
- webscout/Bing_search.py +417 -417
- webscout/DWEBS.py +529 -529
- webscout/Extra/Act.md +309 -309
- webscout/Extra/GitToolkit/__init__.py +10 -10
- webscout/Extra/GitToolkit/gitapi/README.md +110 -110
- webscout/Extra/GitToolkit/gitapi/__init__.py +11 -11
- webscout/Extra/GitToolkit/gitapi/repository.py +195 -195
- webscout/Extra/GitToolkit/gitapi/user.py +96 -96
- webscout/Extra/GitToolkit/gitapi/utils.py +61 -61
- webscout/Extra/YTToolkit/README.md +375 -375
- webscout/Extra/YTToolkit/YTdownloader.py +956 -956
- webscout/Extra/YTToolkit/__init__.py +2 -2
- webscout/Extra/YTToolkit/transcriber.py +475 -475
- webscout/Extra/YTToolkit/ytapi/README.md +44 -44
- webscout/Extra/YTToolkit/ytapi/__init__.py +6 -6
- webscout/Extra/YTToolkit/ytapi/channel.py +307 -307
- webscout/Extra/YTToolkit/ytapi/errors.py +13 -13
- webscout/Extra/YTToolkit/ytapi/extras.py +118 -118
- webscout/Extra/YTToolkit/ytapi/https.py +88 -88
- webscout/Extra/YTToolkit/ytapi/patterns.py +61 -61
- webscout/Extra/YTToolkit/ytapi/playlist.py +58 -58
- webscout/Extra/YTToolkit/ytapi/pool.py +7 -7
- webscout/Extra/YTToolkit/ytapi/query.py +39 -39
- webscout/Extra/YTToolkit/ytapi/stream.py +62 -62
- webscout/Extra/YTToolkit/ytapi/utils.py +62 -62
- webscout/Extra/YTToolkit/ytapi/video.py +232 -232
- webscout/Extra/autocoder/__init__.py +9 -9
- webscout/Extra/autocoder/autocoder.py +1105 -1105
- webscout/Extra/autocoder/autocoder_utiles.py +332 -332
- webscout/Extra/gguf.md +429 -429
- webscout/Extra/gguf.py +1213 -1213
- webscout/Extra/tempmail/README.md +487 -487
- webscout/Extra/tempmail/__init__.py +27 -27
- webscout/Extra/tempmail/async_utils.py +140 -140
- webscout/Extra/tempmail/base.py +160 -160
- webscout/Extra/tempmail/cli.py +186 -186
- webscout/Extra/tempmail/emailnator.py +84 -84
- webscout/Extra/tempmail/mail_tm.py +360 -360
- webscout/Extra/tempmail/temp_mail_io.py +291 -291
- webscout/Extra/weather.md +281 -281
- webscout/Extra/weather.py +193 -193
- webscout/Litlogger/README.md +10 -10
- webscout/Litlogger/__init__.py +15 -15
- webscout/Litlogger/formats.py +13 -13
- webscout/Litlogger/handlers.py +121 -121
- webscout/Litlogger/levels.py +13 -13
- webscout/Litlogger/logger.py +134 -134
- webscout/Provider/AISEARCH/Perplexity.py +332 -332
- webscout/Provider/AISEARCH/README.md +279 -279
- webscout/Provider/AISEARCH/__init__.py +33 -11
- webscout/Provider/AISEARCH/felo_search.py +206 -206
- webscout/Provider/AISEARCH/genspark_search.py +323 -323
- webscout/Provider/AISEARCH/hika_search.py +185 -185
- webscout/Provider/AISEARCH/iask_search.py +410 -410
- webscout/Provider/AISEARCH/monica_search.py +219 -219
- webscout/Provider/AISEARCH/scira_search.py +316 -314
- webscout/Provider/AISEARCH/stellar_search.py +177 -177
- webscout/Provider/AISEARCH/webpilotai_search.py +255 -255
- webscout/Provider/Aitopia.py +314 -315
- webscout/Provider/Andi.py +3 -3
- webscout/Provider/Apriel.py +306 -0
- webscout/Provider/ChatGPTClone.py +236 -236
- webscout/Provider/ChatSandbox.py +343 -342
- webscout/Provider/Cloudflare.py +324 -324
- webscout/Provider/Cohere.py +208 -207
- webscout/Provider/Deepinfra.py +370 -369
- webscout/Provider/ExaAI.py +260 -260
- webscout/Provider/ExaChat.py +308 -387
- webscout/Provider/Flowith.py +221 -221
- webscout/Provider/GMI.py +293 -0
- webscout/Provider/Gemini.py +164 -162
- webscout/Provider/GeminiProxy.py +167 -166
- webscout/Provider/GithubChat.py +371 -370
- webscout/Provider/Groq.py +800 -800
- webscout/Provider/HeckAI.py +383 -379
- webscout/Provider/Jadve.py +282 -297
- webscout/Provider/K2Think.py +308 -0
- webscout/Provider/Koboldai.py +206 -384
- webscout/Provider/LambdaChat.py +423 -425
- webscout/Provider/Nemotron.py +244 -245
- webscout/Provider/Netwrck.py +248 -247
- webscout/Provider/OLLAMA.py +395 -394
- webscout/Provider/OPENAI/Cloudflare.py +394 -395
- webscout/Provider/OPENAI/FalconH1.py +452 -457
- webscout/Provider/OPENAI/FreeGemini.py +297 -299
- webscout/Provider/OPENAI/{monochat.py → K2Think.py} +432 -329
- webscout/Provider/OPENAI/NEMOTRON.py +241 -244
- webscout/Provider/OPENAI/PI.py +428 -427
- webscout/Provider/OPENAI/README.md +959 -959
- webscout/Provider/OPENAI/TogetherAI.py +345 -345
- webscout/Provider/OPENAI/TwoAI.py +466 -467
- webscout/Provider/OPENAI/__init__.py +33 -59
- webscout/Provider/OPENAI/ai4chat.py +313 -303
- webscout/Provider/OPENAI/base.py +249 -269
- webscout/Provider/OPENAI/chatglm.py +528 -0
- webscout/Provider/OPENAI/chatgpt.py +593 -588
- webscout/Provider/OPENAI/chatgptclone.py +521 -524
- webscout/Provider/OPENAI/chatsandbox.py +202 -177
- webscout/Provider/OPENAI/deepinfra.py +319 -315
- webscout/Provider/OPENAI/e2b.py +1665 -1665
- webscout/Provider/OPENAI/exaai.py +420 -420
- webscout/Provider/OPENAI/exachat.py +452 -452
- webscout/Provider/OPENAI/friendli.py +232 -232
- webscout/Provider/OPENAI/{refact.py → gmi.py} +324 -274
- webscout/Provider/OPENAI/groq.py +364 -364
- webscout/Provider/OPENAI/heckai.py +314 -311
- webscout/Provider/OPENAI/llmchatco.py +337 -337
- webscout/Provider/OPENAI/netwrck.py +355 -354
- webscout/Provider/OPENAI/oivscode.py +290 -290
- webscout/Provider/OPENAI/opkfc.py +518 -518
- webscout/Provider/OPENAI/pydantic_imports.py +1 -1
- webscout/Provider/OPENAI/scirachat.py +535 -529
- webscout/Provider/OPENAI/sonus.py +308 -308
- webscout/Provider/OPENAI/standardinput.py +442 -442
- webscout/Provider/OPENAI/textpollinations.py +340 -348
- webscout/Provider/OPENAI/toolbaz.py +419 -413
- webscout/Provider/OPENAI/typefully.py +362 -362
- webscout/Provider/OPENAI/utils.py +295 -295
- webscout/Provider/OPENAI/venice.py +436 -436
- webscout/Provider/OPENAI/wisecat.py +387 -387
- webscout/Provider/OPENAI/writecream.py +166 -166
- webscout/Provider/OPENAI/x0gpt.py +378 -378
- webscout/Provider/OPENAI/yep.py +389 -389
- webscout/Provider/OpenGPT.py +230 -230
- webscout/Provider/Openai.py +244 -496
- webscout/Provider/PI.py +405 -404
- webscout/Provider/Perplexitylabs.py +430 -431
- webscout/Provider/QwenLM.py +272 -254
- webscout/Provider/STT/__init__.py +32 -2
- webscout/Provider/{Llama3.py → Sambanova.py} +257 -258
- webscout/Provider/StandardInput.py +309 -309
- webscout/Provider/TTI/README.md +82 -82
- webscout/Provider/TTI/__init__.py +33 -12
- webscout/Provider/TTI/aiarta.py +413 -413
- webscout/Provider/TTI/base.py +136 -136
- webscout/Provider/TTI/bing.py +243 -243
- webscout/Provider/TTI/gpt1image.py +149 -149
- webscout/Provider/TTI/imagen.py +196 -196
- webscout/Provider/TTI/infip.py +211 -211
- webscout/Provider/TTI/magicstudio.py +232 -232
- webscout/Provider/TTI/monochat.py +219 -219
- webscout/Provider/TTI/piclumen.py +214 -214
- webscout/Provider/TTI/pixelmuse.py +232 -232
- webscout/Provider/TTI/pollinations.py +232 -232
- webscout/Provider/TTI/together.py +288 -288
- webscout/Provider/TTI/utils.py +12 -12
- webscout/Provider/TTI/venice.py +367 -367
- webscout/Provider/TTS/README.md +192 -192
- webscout/Provider/TTS/__init__.py +33 -10
- webscout/Provider/TTS/parler.py +110 -110
- webscout/Provider/TTS/streamElements.py +333 -333
- webscout/Provider/TTS/utils.py +280 -280
- webscout/Provider/TeachAnything.py +237 -236
- webscout/Provider/TextPollinationsAI.py +311 -318
- webscout/Provider/TogetherAI.py +356 -357
- webscout/Provider/TwoAI.py +313 -569
- webscout/Provider/TypliAI.py +312 -311
- webscout/Provider/UNFINISHED/ChatHub.py +208 -208
- webscout/Provider/UNFINISHED/ChutesAI.py +313 -313
- webscout/Provider/{GizAI.py → UNFINISHED/GizAI.py} +294 -294
- webscout/Provider/{Marcus.py → UNFINISHED/Marcus.py} +198 -198
- webscout/Provider/{Qodo.py → UNFINISHED/Qodo.py} +477 -477
- webscout/Provider/UNFINISHED/VercelAIGateway.py +338 -338
- webscout/Provider/{XenAI.py → UNFINISHED/XenAI.py} +324 -324
- webscout/Provider/UNFINISHED/Youchat.py +330 -330
- webscout/Provider/UNFINISHED/liner.py +334 -0
- webscout/Provider/UNFINISHED/liner_api_request.py +262 -262
- webscout/Provider/UNFINISHED/puterjs.py +634 -634
- webscout/Provider/UNFINISHED/samurai.py +223 -223
- webscout/Provider/UNFINISHED/test_lmarena.py +119 -119
- webscout/Provider/Venice.py +251 -250
- webscout/Provider/VercelAI.py +256 -255
- webscout/Provider/WiseCat.py +232 -231
- webscout/Provider/WrDoChat.py +367 -366
- webscout/Provider/__init__.py +33 -86
- webscout/Provider/ai4chat.py +174 -174
- webscout/Provider/akashgpt.py +331 -334
- webscout/Provider/cerebras.py +446 -340
- webscout/Provider/chatglm.py +394 -214
- webscout/Provider/cleeai.py +211 -212
- webscout/Provider/deepseek_assistant.py +1 -1
- webscout/Provider/elmo.py +282 -282
- webscout/Provider/geminiapi.py +208 -208
- webscout/Provider/granite.py +261 -261
- webscout/Provider/hermes.py +263 -265
- webscout/Provider/julius.py +223 -222
- webscout/Provider/learnfastai.py +309 -309
- webscout/Provider/llama3mitril.py +214 -214
- webscout/Provider/llmchat.py +243 -243
- webscout/Provider/llmchatco.py +290 -290
- webscout/Provider/meta.py +801 -801
- webscout/Provider/oivscode.py +309 -309
- webscout/Provider/scira_chat.py +384 -457
- webscout/Provider/searchchat.py +292 -291
- webscout/Provider/sonus.py +258 -258
- webscout/Provider/toolbaz.py +370 -364
- webscout/Provider/turboseek.py +274 -265
- webscout/Provider/typefully.py +208 -207
- webscout/Provider/x0gpt.py +1 -0
- webscout/Provider/yep.py +372 -371
- webscout/__init__.py +30 -31
- webscout/__main__.py +5 -5
- webscout/auth/api_key_manager.py +189 -189
- webscout/auth/config.py +175 -175
- webscout/auth/models.py +185 -185
- webscout/auth/routes.py +664 -664
- webscout/auth/simple_logger.py +236 -236
- webscout/cli.py +523 -523
- webscout/conversation.py +438 -438
- webscout/exceptions.py +361 -361
- webscout/litagent/Readme.md +298 -298
- webscout/litagent/__init__.py +28 -28
- webscout/litagent/agent.py +581 -581
- webscout/litagent/constants.py +59 -59
- webscout/litprinter/__init__.py +58 -58
- webscout/models.py +181 -181
- webscout/optimizers.py +419 -419
- webscout/prompt_manager.py +288 -288
- webscout/sanitize.py +1078 -1078
- webscout/scout/README.md +401 -401
- webscout/scout/__init__.py +8 -8
- webscout/scout/core/__init__.py +6 -6
- webscout/scout/core/crawler.py +297 -297
- webscout/scout/core/scout.py +706 -706
- webscout/scout/core/search_result.py +95 -95
- webscout/scout/core/text_analyzer.py +62 -62
- webscout/scout/core/text_utils.py +277 -277
- webscout/scout/core/web_analyzer.py +51 -51
- webscout/scout/element.py +599 -599
- webscout/scout/parsers/__init__.py +69 -69
- webscout/scout/parsers/html5lib_parser.py +172 -172
- webscout/scout/parsers/html_parser.py +236 -236
- webscout/scout/parsers/lxml_parser.py +178 -178
- webscout/scout/utils.py +37 -37
- webscout/swiftcli/Readme.md +323 -323
- webscout/swiftcli/__init__.py +95 -95
- webscout/swiftcli/core/__init__.py +7 -7
- webscout/swiftcli/core/cli.py +308 -308
- webscout/swiftcli/core/context.py +104 -104
- webscout/swiftcli/core/group.py +241 -241
- webscout/swiftcli/decorators/__init__.py +28 -28
- webscout/swiftcli/decorators/command.py +221 -221
- webscout/swiftcli/decorators/options.py +220 -220
- webscout/swiftcli/decorators/output.py +302 -302
- webscout/swiftcli/exceptions.py +21 -21
- webscout/swiftcli/plugins/__init__.py +9 -9
- webscout/swiftcli/plugins/base.py +135 -135
- webscout/swiftcli/plugins/manager.py +269 -269
- webscout/swiftcli/utils/__init__.py +59 -59
- webscout/swiftcli/utils/formatting.py +252 -252
- webscout/swiftcli/utils/parsing.py +267 -267
- webscout/update_checker.py +117 -117
- webscout/version.py +1 -1
- webscout/webscout_search.py +1183 -1183
- webscout/webscout_search_async.py +649 -649
- webscout/yep_search.py +346 -346
- webscout/zeroart/README.md +89 -89
- webscout/zeroart/__init__.py +134 -134
- webscout/zeroart/base.py +66 -66
- webscout/zeroart/effects.py +100 -100
- webscout/zeroart/fonts.py +1238 -1238
- {webscout-8.3.6.dist-info → webscout-2025.10.11.dist-info}/METADATA +937 -936
- webscout-2025.10.11.dist-info/RECORD +300 -0
- webscout/Provider/AISEARCH/DeepFind.py +0 -254
- webscout/Provider/AllenAI.py +0 -440
- webscout/Provider/Blackboxai.py +0 -793
- webscout/Provider/FreeGemini.py +0 -250
- webscout/Provider/GptOss.py +0 -207
- webscout/Provider/Hunyuan.py +0 -283
- webscout/Provider/Kimi.py +0 -445
- webscout/Provider/MCPCore.py +0 -322
- webscout/Provider/MiniMax.py +0 -207
- webscout/Provider/OPENAI/BLACKBOXAI.py +0 -1045
- webscout/Provider/OPENAI/MiniMax.py +0 -298
- webscout/Provider/OPENAI/Qwen3.py +0 -304
- webscout/Provider/OPENAI/autoproxy.py +0 -1067
- webscout/Provider/OPENAI/copilot.py +0 -321
- webscout/Provider/OPENAI/gptoss.py +0 -288
- webscout/Provider/OPENAI/kimi.py +0 -469
- webscout/Provider/OPENAI/mcpcore.py +0 -431
- webscout/Provider/OPENAI/multichat.py +0 -378
- webscout/Provider/OPENAI/qodo.py +0 -630
- webscout/Provider/OPENAI/xenai.py +0 -514
- webscout/Provider/Reka.py +0 -214
- webscout/Provider/UNFINISHED/fetch_together_models.py +0 -90
- webscout/Provider/asksteve.py +0 -220
- webscout/Provider/copilot.py +0 -441
- webscout/Provider/freeaichat.py +0 -294
- webscout/Provider/koala.py +0 -182
- webscout/Provider/lmarena.py +0 -198
- webscout/Provider/monochat.py +0 -275
- webscout/Provider/multichat.py +0 -375
- webscout/Provider/scnet.py +0 -244
- webscout/Provider/talkai.py +0 -194
- webscout/tempid.py +0 -128
- webscout-8.3.6.dist-info/RECORD +0 -327
- {webscout-8.3.6.dist-info → webscout-2025.10.11.dist-info}/WHEEL +0 -0
- {webscout-8.3.6.dist-info → webscout-2025.10.11.dist-info}/entry_points.txt +0 -0
- {webscout-8.3.6.dist-info → webscout-2025.10.11.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.3.6.dist-info → webscout-2025.10.11.dist-info}/top_level.txt +0 -0
|
@@ -1,307 +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)))
|
|
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)))
|
|
@@ -1,13 +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
|
|
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
|