webscout 8.2.9__py3-none-any.whl → 2026.1.19__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.
- webscout/AIauto.py +524 -251
- webscout/AIbase.py +247 -319
- webscout/AIutel.py +68 -703
- webscout/Bard.py +1072 -1026
- webscout/Extra/GitToolkit/__init__.py +10 -10
- webscout/Extra/GitToolkit/gitapi/__init__.py +20 -12
- webscout/Extra/GitToolkit/gitapi/gist.py +142 -0
- webscout/Extra/GitToolkit/gitapi/organization.py +91 -0
- webscout/Extra/GitToolkit/gitapi/repository.py +308 -195
- webscout/Extra/GitToolkit/gitapi/search.py +162 -0
- webscout/Extra/GitToolkit/gitapi/trending.py +236 -0
- webscout/Extra/GitToolkit/gitapi/user.py +128 -96
- webscout/Extra/GitToolkit/gitapi/utils.py +82 -62
- webscout/Extra/YTToolkit/README.md +443 -375
- webscout/Extra/YTToolkit/YTdownloader.py +953 -957
- webscout/Extra/YTToolkit/__init__.py +3 -3
- webscout/Extra/YTToolkit/transcriber.py +595 -476
- webscout/Extra/YTToolkit/ytapi/README.md +230 -44
- webscout/Extra/YTToolkit/ytapi/__init__.py +22 -6
- webscout/Extra/YTToolkit/ytapi/captions.py +190 -0
- webscout/Extra/YTToolkit/ytapi/channel.py +302 -307
- webscout/Extra/YTToolkit/ytapi/errors.py +13 -13
- webscout/Extra/YTToolkit/ytapi/extras.py +178 -118
- webscout/Extra/YTToolkit/ytapi/hashtag.py +120 -0
- webscout/Extra/YTToolkit/ytapi/https.py +89 -88
- webscout/Extra/YTToolkit/ytapi/patterns.py +61 -61
- webscout/Extra/YTToolkit/ytapi/playlist.py +59 -59
- webscout/Extra/YTToolkit/ytapi/pool.py +8 -8
- webscout/Extra/YTToolkit/ytapi/query.py +143 -40
- webscout/Extra/YTToolkit/ytapi/shorts.py +122 -0
- webscout/Extra/YTToolkit/ytapi/stream.py +68 -63
- webscout/Extra/YTToolkit/ytapi/suggestions.py +97 -0
- webscout/Extra/YTToolkit/ytapi/utils.py +66 -62
- webscout/Extra/YTToolkit/ytapi/video.py +403 -232
- webscout/Extra/__init__.py +2 -3
- webscout/Extra/gguf.py +1298 -684
- webscout/Extra/tempmail/README.md +487 -487
- webscout/Extra/tempmail/__init__.py +28 -28
- webscout/Extra/tempmail/async_utils.py +143 -141
- webscout/Extra/tempmail/base.py +172 -161
- webscout/Extra/tempmail/cli.py +191 -187
- webscout/Extra/tempmail/emailnator.py +88 -84
- webscout/Extra/tempmail/mail_tm.py +378 -361
- webscout/Extra/tempmail/temp_mail_io.py +304 -292
- webscout/Extra/weather.py +196 -194
- webscout/Extra/weather_ascii.py +17 -15
- webscout/Provider/AISEARCH/PERPLEXED_search.py +175 -0
- webscout/Provider/AISEARCH/Perplexity.py +292 -333
- webscout/Provider/AISEARCH/README.md +106 -279
- webscout/Provider/AISEARCH/__init__.py +16 -9
- webscout/Provider/AISEARCH/brave_search.py +298 -0
- webscout/Provider/AISEARCH/iask_search.py +357 -410
- webscout/Provider/AISEARCH/monica_search.py +200 -220
- webscout/Provider/AISEARCH/webpilotai_search.py +242 -255
- webscout/Provider/Algion.py +413 -0
- webscout/Provider/Andi.py +74 -69
- webscout/Provider/Apriel.py +313 -0
- webscout/Provider/Ayle.py +323 -0
- webscout/Provider/ChatSandbox.py +329 -342
- webscout/Provider/ClaudeOnline.py +365 -0
- webscout/Provider/Cohere.py +232 -208
- webscout/Provider/DeepAI.py +367 -0
- webscout/Provider/Deepinfra.py +467 -340
- webscout/Provider/EssentialAI.py +217 -0
- webscout/Provider/ExaAI.py +274 -261
- webscout/Provider/Gemini.py +175 -169
- webscout/Provider/GithubChat.py +385 -369
- webscout/Provider/Gradient.py +286 -0
- webscout/Provider/Groq.py +556 -801
- webscout/Provider/HadadXYZ.py +323 -0
- webscout/Provider/HeckAI.py +392 -375
- webscout/Provider/HuggingFace.py +387 -0
- webscout/Provider/IBM.py +340 -0
- webscout/Provider/Jadve.py +317 -291
- webscout/Provider/K2Think.py +306 -0
- webscout/Provider/Koboldai.py +221 -384
- webscout/Provider/Netwrck.py +273 -270
- webscout/Provider/Nvidia.py +310 -0
- webscout/Provider/OPENAI/DeepAI.py +489 -0
- webscout/Provider/OPENAI/K2Think.py +423 -0
- webscout/Provider/OPENAI/PI.py +463 -0
- webscout/Provider/OPENAI/README.md +890 -952
- webscout/Provider/OPENAI/TogetherAI.py +405 -0
- webscout/Provider/OPENAI/TwoAI.py +255 -357
- webscout/Provider/OPENAI/__init__.py +148 -40
- webscout/Provider/OPENAI/ai4chat.py +348 -293
- webscout/Provider/OPENAI/akashgpt.py +436 -0
- webscout/Provider/OPENAI/algion.py +303 -0
- webscout/Provider/OPENAI/{exachat.py → ayle.py} +365 -444
- webscout/Provider/OPENAI/base.py +253 -249
- webscout/Provider/OPENAI/cerebras.py +296 -0
- webscout/Provider/OPENAI/chatgpt.py +870 -556
- webscout/Provider/OPENAI/chatsandbox.py +233 -173
- webscout/Provider/OPENAI/deepinfra.py +403 -322
- webscout/Provider/OPENAI/e2b.py +2370 -1414
- webscout/Provider/OPENAI/elmo.py +278 -0
- webscout/Provider/OPENAI/exaai.py +452 -417
- webscout/Provider/OPENAI/freeassist.py +446 -0
- webscout/Provider/OPENAI/gradient.py +448 -0
- webscout/Provider/OPENAI/groq.py +380 -364
- webscout/Provider/OPENAI/hadadxyz.py +292 -0
- webscout/Provider/OPENAI/heckai.py +333 -308
- webscout/Provider/OPENAI/huggingface.py +321 -0
- webscout/Provider/OPENAI/ibm.py +425 -0
- webscout/Provider/OPENAI/llmchat.py +253 -0
- webscout/Provider/OPENAI/llmchatco.py +378 -335
- webscout/Provider/OPENAI/meta.py +541 -0
- webscout/Provider/OPENAI/netwrck.py +374 -357
- webscout/Provider/OPENAI/nvidia.py +317 -0
- webscout/Provider/OPENAI/oivscode.py +348 -287
- webscout/Provider/OPENAI/openrouter.py +328 -0
- webscout/Provider/OPENAI/pydantic_imports.py +1 -172
- webscout/Provider/OPENAI/sambanova.py +397 -0
- webscout/Provider/OPENAI/sonus.py +305 -304
- webscout/Provider/OPENAI/textpollinations.py +370 -339
- webscout/Provider/OPENAI/toolbaz.py +375 -413
- webscout/Provider/OPENAI/typefully.py +419 -355
- webscout/Provider/OPENAI/typliai.py +279 -0
- webscout/Provider/OPENAI/utils.py +314 -318
- webscout/Provider/OPENAI/wisecat.py +359 -387
- webscout/Provider/OPENAI/writecream.py +185 -163
- webscout/Provider/OPENAI/x0gpt.py +462 -365
- webscout/Provider/OPENAI/zenmux.py +380 -0
- webscout/Provider/OpenRouter.py +386 -0
- webscout/Provider/Openai.py +337 -496
- webscout/Provider/PI.py +443 -429
- webscout/Provider/QwenLM.py +346 -254
- webscout/Provider/STT/__init__.py +28 -0
- webscout/Provider/STT/base.py +303 -0
- webscout/Provider/STT/elevenlabs.py +264 -0
- webscout/Provider/Sambanova.py +317 -0
- webscout/Provider/TTI/README.md +69 -82
- webscout/Provider/TTI/__init__.py +37 -7
- webscout/Provider/TTI/base.py +147 -64
- webscout/Provider/TTI/claudeonline.py +393 -0
- webscout/Provider/TTI/magicstudio.py +292 -201
- webscout/Provider/TTI/miragic.py +180 -0
- webscout/Provider/TTI/pollinations.py +331 -221
- webscout/Provider/TTI/together.py +334 -0
- webscout/Provider/TTI/utils.py +14 -11
- webscout/Provider/TTS/README.md +186 -192
- webscout/Provider/TTS/__init__.py +43 -10
- webscout/Provider/TTS/base.py +523 -159
- webscout/Provider/TTS/deepgram.py +286 -156
- webscout/Provider/TTS/elevenlabs.py +189 -111
- webscout/Provider/TTS/freetts.py +218 -0
- webscout/Provider/TTS/murfai.py +288 -113
- webscout/Provider/TTS/openai_fm.py +364 -129
- webscout/Provider/TTS/parler.py +203 -111
- webscout/Provider/TTS/qwen.py +334 -0
- webscout/Provider/TTS/sherpa.py +286 -0
- webscout/Provider/TTS/speechma.py +693 -580
- webscout/Provider/TTS/streamElements.py +275 -333
- webscout/Provider/TTS/utils.py +280 -280
- webscout/Provider/TextPollinationsAI.py +331 -308
- webscout/Provider/TogetherAI.py +450 -0
- webscout/Provider/TwoAI.py +309 -475
- webscout/Provider/TypliAI.py +311 -305
- webscout/Provider/UNFINISHED/ChatHub.py +219 -209
- webscout/Provider/{OPENAI/glider.py → UNFINISHED/ChutesAI.py} +331 -326
- webscout/Provider/{GizAI.py → UNFINISHED/GizAI.py} +300 -295
- webscout/Provider/{Marcus.py → UNFINISHED/Marcus.py} +218 -198
- webscout/Provider/UNFINISHED/Qodo.py +481 -0
- webscout/Provider/{MCPCore.py → UNFINISHED/XenAI.py} +330 -315
- webscout/Provider/UNFINISHED/Youchat.py +347 -330
- webscout/Provider/UNFINISHED/aihumanizer.py +41 -0
- webscout/Provider/UNFINISHED/grammerchecker.py +37 -0
- webscout/Provider/UNFINISHED/liner.py +342 -0
- webscout/Provider/UNFINISHED/liner_api_request.py +246 -263
- webscout/Provider/{samurai.py → UNFINISHED/samurai.py} +231 -224
- webscout/Provider/WiseCat.py +256 -233
- webscout/Provider/WrDoChat.py +390 -370
- webscout/Provider/__init__.py +115 -174
- webscout/Provider/ai4chat.py +181 -174
- webscout/Provider/akashgpt.py +330 -335
- webscout/Provider/cerebras.py +397 -290
- webscout/Provider/cleeai.py +236 -213
- webscout/Provider/elmo.py +291 -283
- webscout/Provider/geminiapi.py +343 -208
- webscout/Provider/julius.py +245 -223
- webscout/Provider/learnfastai.py +333 -325
- webscout/Provider/llama3mitril.py +230 -215
- webscout/Provider/llmchat.py +308 -258
- webscout/Provider/llmchatco.py +321 -306
- webscout/Provider/meta.py +996 -801
- webscout/Provider/oivscode.py +332 -309
- webscout/Provider/searchchat.py +316 -292
- webscout/Provider/sonus.py +264 -258
- webscout/Provider/toolbaz.py +359 -353
- webscout/Provider/turboseek.py +332 -266
- webscout/Provider/typefully.py +262 -202
- webscout/Provider/x0gpt.py +332 -299
- webscout/__init__.py +31 -39
- webscout/__main__.py +5 -5
- webscout/cli.py +585 -524
- webscout/client.py +1497 -70
- webscout/conversation.py +140 -436
- webscout/exceptions.py +383 -362
- webscout/litagent/__init__.py +29 -29
- webscout/litagent/agent.py +492 -455
- webscout/litagent/constants.py +60 -60
- webscout/models.py +505 -181
- webscout/optimizers.py +74 -420
- webscout/prompt_manager.py +376 -288
- webscout/sanitize.py +1514 -0
- webscout/scout/README.md +452 -404
- webscout/scout/__init__.py +8 -8
- webscout/scout/core/__init__.py +7 -7
- webscout/scout/core/crawler.py +330 -210
- webscout/scout/core/scout.py +800 -607
- webscout/scout/core/search_result.py +51 -96
- webscout/scout/core/text_analyzer.py +64 -63
- webscout/scout/core/text_utils.py +412 -277
- webscout/scout/core/web_analyzer.py +54 -52
- webscout/scout/element.py +872 -478
- webscout/scout/parsers/__init__.py +70 -69
- webscout/scout/parsers/html5lib_parser.py +182 -172
- webscout/scout/parsers/html_parser.py +238 -236
- webscout/scout/parsers/lxml_parser.py +203 -178
- webscout/scout/utils.py +38 -37
- webscout/search/__init__.py +47 -0
- webscout/search/base.py +201 -0
- webscout/search/bing_main.py +45 -0
- webscout/search/brave_main.py +92 -0
- webscout/search/duckduckgo_main.py +57 -0
- webscout/search/engines/__init__.py +127 -0
- webscout/search/engines/bing/__init__.py +15 -0
- webscout/search/engines/bing/base.py +35 -0
- webscout/search/engines/bing/images.py +114 -0
- webscout/search/engines/bing/news.py +96 -0
- webscout/search/engines/bing/suggestions.py +36 -0
- webscout/search/engines/bing/text.py +109 -0
- webscout/search/engines/brave/__init__.py +19 -0
- webscout/search/engines/brave/base.py +47 -0
- webscout/search/engines/brave/images.py +213 -0
- webscout/search/engines/brave/news.py +353 -0
- webscout/search/engines/brave/suggestions.py +318 -0
- webscout/search/engines/brave/text.py +167 -0
- webscout/search/engines/brave/videos.py +364 -0
- webscout/search/engines/duckduckgo/__init__.py +25 -0
- webscout/search/engines/duckduckgo/answers.py +80 -0
- webscout/search/engines/duckduckgo/base.py +189 -0
- webscout/search/engines/duckduckgo/images.py +100 -0
- webscout/search/engines/duckduckgo/maps.py +183 -0
- webscout/search/engines/duckduckgo/news.py +70 -0
- webscout/search/engines/duckduckgo/suggestions.py +22 -0
- webscout/search/engines/duckduckgo/text.py +221 -0
- webscout/search/engines/duckduckgo/translate.py +48 -0
- webscout/search/engines/duckduckgo/videos.py +80 -0
- webscout/search/engines/duckduckgo/weather.py +84 -0
- webscout/search/engines/mojeek.py +61 -0
- webscout/search/engines/wikipedia.py +77 -0
- webscout/search/engines/yahoo/__init__.py +41 -0
- webscout/search/engines/yahoo/answers.py +19 -0
- webscout/search/engines/yahoo/base.py +34 -0
- webscout/search/engines/yahoo/images.py +323 -0
- webscout/search/engines/yahoo/maps.py +19 -0
- webscout/search/engines/yahoo/news.py +258 -0
- webscout/search/engines/yahoo/suggestions.py +140 -0
- webscout/search/engines/yahoo/text.py +273 -0
- webscout/search/engines/yahoo/translate.py +19 -0
- webscout/search/engines/yahoo/videos.py +302 -0
- webscout/search/engines/yahoo/weather.py +220 -0
- webscout/search/engines/yandex.py +67 -0
- webscout/search/engines/yep/__init__.py +13 -0
- webscout/search/engines/yep/base.py +34 -0
- webscout/search/engines/yep/images.py +101 -0
- webscout/search/engines/yep/suggestions.py +38 -0
- webscout/search/engines/yep/text.py +99 -0
- webscout/search/http_client.py +172 -0
- webscout/search/results.py +141 -0
- webscout/search/yahoo_main.py +57 -0
- webscout/search/yep_main.py +48 -0
- webscout/server/__init__.py +48 -0
- webscout/server/config.py +78 -0
- webscout/server/exceptions.py +69 -0
- webscout/server/providers.py +286 -0
- webscout/server/request_models.py +131 -0
- webscout/server/request_processing.py +404 -0
- webscout/server/routes.py +642 -0
- webscout/server/server.py +351 -0
- webscout/server/ui_templates.py +1171 -0
- webscout/swiftcli/__init__.py +79 -95
- webscout/swiftcli/core/__init__.py +7 -7
- webscout/swiftcli/core/cli.py +574 -297
- webscout/swiftcli/core/context.py +98 -104
- webscout/swiftcli/core/group.py +268 -241
- webscout/swiftcli/decorators/__init__.py +28 -28
- webscout/swiftcli/decorators/command.py +243 -221
- webscout/swiftcli/decorators/options.py +247 -220
- webscout/swiftcli/decorators/output.py +392 -252
- webscout/swiftcli/exceptions.py +21 -21
- webscout/swiftcli/plugins/__init__.py +9 -9
- webscout/swiftcli/plugins/base.py +134 -135
- webscout/swiftcli/plugins/manager.py +269 -269
- webscout/swiftcli/utils/__init__.py +58 -59
- webscout/swiftcli/utils/formatting.py +251 -252
- webscout/swiftcli/utils/parsing.py +368 -267
- webscout/update_checker.py +280 -136
- webscout/utils.py +28 -14
- webscout/version.py +2 -1
- webscout/version.py.bak +3 -0
- webscout/zeroart/__init__.py +218 -135
- webscout/zeroart/base.py +70 -66
- webscout/zeroart/effects.py +155 -101
- webscout/zeroart/fonts.py +1799 -1239
- webscout-2026.1.19.dist-info/METADATA +638 -0
- webscout-2026.1.19.dist-info/RECORD +312 -0
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/WHEEL +1 -1
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/entry_points.txt +1 -1
- webscout/DWEBS.py +0 -520
- webscout/Extra/Act.md +0 -309
- webscout/Extra/GitToolkit/gitapi/README.md +0 -110
- webscout/Extra/autocoder/__init__.py +0 -9
- webscout/Extra/autocoder/autocoder.py +0 -1105
- webscout/Extra/autocoder/autocoder_utiles.py +0 -332
- webscout/Extra/gguf.md +0 -430
- webscout/Extra/weather.md +0 -281
- webscout/Litlogger/README.md +0 -10
- webscout/Litlogger/__init__.py +0 -15
- webscout/Litlogger/formats.py +0 -4
- webscout/Litlogger/handlers.py +0 -103
- webscout/Litlogger/levels.py +0 -13
- webscout/Litlogger/logger.py +0 -92
- webscout/Provider/AI21.py +0 -177
- webscout/Provider/AISEARCH/DeepFind.py +0 -254
- webscout/Provider/AISEARCH/felo_search.py +0 -202
- webscout/Provider/AISEARCH/genspark_search.py +0 -324
- webscout/Provider/AISEARCH/hika_search.py +0 -186
- webscout/Provider/AISEARCH/scira_search.py +0 -298
- webscout/Provider/Aitopia.py +0 -316
- webscout/Provider/AllenAI.py +0 -440
- webscout/Provider/Blackboxai.py +0 -791
- webscout/Provider/ChatGPTClone.py +0 -237
- webscout/Provider/ChatGPTGratis.py +0 -194
- webscout/Provider/Cloudflare.py +0 -324
- webscout/Provider/ExaChat.py +0 -358
- webscout/Provider/Flowith.py +0 -217
- webscout/Provider/FreeGemini.py +0 -250
- webscout/Provider/Glider.py +0 -225
- webscout/Provider/HF_space/__init__.py +0 -0
- webscout/Provider/HF_space/qwen_qwen2.py +0 -206
- webscout/Provider/HuggingFaceChat.py +0 -469
- webscout/Provider/Hunyuan.py +0 -283
- webscout/Provider/LambdaChat.py +0 -411
- webscout/Provider/Llama3.py +0 -259
- webscout/Provider/Nemotron.py +0 -218
- webscout/Provider/OLLAMA.py +0 -396
- webscout/Provider/OPENAI/BLACKBOXAI.py +0 -766
- webscout/Provider/OPENAI/Cloudflare.py +0 -378
- webscout/Provider/OPENAI/FreeGemini.py +0 -283
- webscout/Provider/OPENAI/NEMOTRON.py +0 -232
- webscout/Provider/OPENAI/Qwen3.py +0 -283
- webscout/Provider/OPENAI/api.py +0 -969
- webscout/Provider/OPENAI/c4ai.py +0 -373
- webscout/Provider/OPENAI/chatgptclone.py +0 -494
- webscout/Provider/OPENAI/copilot.py +0 -242
- webscout/Provider/OPENAI/flowith.py +0 -162
- webscout/Provider/OPENAI/freeaichat.py +0 -359
- webscout/Provider/OPENAI/mcpcore.py +0 -389
- webscout/Provider/OPENAI/multichat.py +0 -376
- webscout/Provider/OPENAI/opkfc.py +0 -496
- webscout/Provider/OPENAI/scirachat.py +0 -477
- webscout/Provider/OPENAI/standardinput.py +0 -433
- webscout/Provider/OPENAI/typegpt.py +0 -364
- webscout/Provider/OPENAI/uncovrAI.py +0 -463
- webscout/Provider/OPENAI/venice.py +0 -431
- webscout/Provider/OPENAI/yep.py +0 -382
- webscout/Provider/OpenGPT.py +0 -209
- webscout/Provider/Perplexitylabs.py +0 -415
- webscout/Provider/Reka.py +0 -214
- webscout/Provider/StandardInput.py +0 -290
- webscout/Provider/TTI/aiarta.py +0 -365
- webscout/Provider/TTI/artbit.py +0 -0
- webscout/Provider/TTI/fastflux.py +0 -200
- webscout/Provider/TTI/piclumen.py +0 -203
- webscout/Provider/TTI/pixelmuse.py +0 -225
- webscout/Provider/TTS/gesserit.py +0 -128
- webscout/Provider/TTS/sthir.py +0 -94
- webscout/Provider/TeachAnything.py +0 -229
- webscout/Provider/UNFINISHED/puterjs.py +0 -635
- webscout/Provider/UNFINISHED/test_lmarena.py +0 -119
- webscout/Provider/Venice.py +0 -258
- webscout/Provider/VercelAI.py +0 -253
- webscout/Provider/Writecream.py +0 -246
- webscout/Provider/WritingMate.py +0 -269
- webscout/Provider/asksteve.py +0 -220
- webscout/Provider/chatglm.py +0 -215
- webscout/Provider/copilot.py +0 -425
- webscout/Provider/freeaichat.py +0 -285
- webscout/Provider/granite.py +0 -235
- webscout/Provider/hermes.py +0 -266
- webscout/Provider/koala.py +0 -170
- webscout/Provider/lmarena.py +0 -198
- webscout/Provider/multichat.py +0 -364
- webscout/Provider/scira_chat.py +0 -299
- webscout/Provider/scnet.py +0 -243
- webscout/Provider/talkai.py +0 -194
- webscout/Provider/typegpt.py +0 -289
- webscout/Provider/uncovr.py +0 -368
- webscout/Provider/yep.py +0 -389
- webscout/litagent/Readme.md +0 -276
- webscout/litprinter/__init__.py +0 -59
- webscout/swiftcli/Readme.md +0 -323
- webscout/tempid.py +0 -128
- webscout/webscout_search.py +0 -1184
- webscout/webscout_search_async.py +0 -654
- webscout/yep_search.py +0 -347
- webscout/zeroart/README.md +0 -89
- webscout-8.2.9.dist-info/METADATA +0 -1033
- webscout-8.2.9.dist-info/RECORD +0 -289
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Brave text search."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from time import sleep
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from webscout.scout import Scout
|
|
9
|
+
|
|
10
|
+
from ....search.results import TextResult
|
|
11
|
+
from .base import BraveBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BraveTextSearch(BraveBase):
|
|
15
|
+
"""Brave text/web search."""
|
|
16
|
+
|
|
17
|
+
name = "brave"
|
|
18
|
+
category = "text"
|
|
19
|
+
|
|
20
|
+
def run(self, *args, **kwargs) -> List[TextResult]:
|
|
21
|
+
"""Perform text search on Brave using offset pagination.
|
|
22
|
+
|
|
23
|
+
Uses server-rendered HTML and parses result containers with CSS selectors.
|
|
24
|
+
"""
|
|
25
|
+
from typing import cast
|
|
26
|
+
|
|
27
|
+
keywords = args[0] if args else kwargs.get("keywords")
|
|
28
|
+
if not keywords:
|
|
29
|
+
raise ValueError("Keywords are mandatory")
|
|
30
|
+
|
|
31
|
+
safesearch = args[2] if len(args) > 2 else kwargs.get("safesearch", "moderate")
|
|
32
|
+
max_results = args[3] if len(args) > 3 else kwargs.get("max_results", 10)
|
|
33
|
+
if max_results is None:
|
|
34
|
+
max_results = 10
|
|
35
|
+
|
|
36
|
+
safesearch_map = {"on": "strict", "moderate": "moderate", "off": "off"}
|
|
37
|
+
safesearch_value = safesearch_map.get(safesearch.lower(), "moderate")
|
|
38
|
+
|
|
39
|
+
start_offset = int(kwargs.get("start_offset", 0))
|
|
40
|
+
offset = start_offset
|
|
41
|
+
|
|
42
|
+
fetched_results: List[TextResult] = []
|
|
43
|
+
fetched_hrefs: set[str] = set()
|
|
44
|
+
|
|
45
|
+
def fetch_html(params: dict) -> str:
|
|
46
|
+
url = f"{self.base_url}/search"
|
|
47
|
+
# Merge session headers to include fingerprint (User-Agent, etc.)
|
|
48
|
+
headers = dict(self.session.headers) if getattr(self, "session", None) else {}
|
|
49
|
+
headers.update({
|
|
50
|
+
"Referer": "https://search.brave.com/",
|
|
51
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
52
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
53
|
+
})
|
|
54
|
+
attempts = 3
|
|
55
|
+
backoff = 1.0
|
|
56
|
+
last_exc: Exception | None = None
|
|
57
|
+
for attempt in range(attempts):
|
|
58
|
+
try:
|
|
59
|
+
resp = self.session.get(url, params=params, headers=headers, timeout=self.timeout)
|
|
60
|
+
resp.raise_for_status()
|
|
61
|
+
return resp.text
|
|
62
|
+
except Exception as exc: # network or HTTP errors
|
|
63
|
+
last_exc = exc
|
|
64
|
+
# If it's a 429 / transient server error, back off and retry
|
|
65
|
+
try:
|
|
66
|
+
code = getattr(exc, "code", None) or getattr(exc, "status_code", None)
|
|
67
|
+
except Exception:
|
|
68
|
+
code = None
|
|
69
|
+
if code in (429, 500, 502, 503, 504):
|
|
70
|
+
sleep(backoff)
|
|
71
|
+
backoff *= 2
|
|
72
|
+
continue
|
|
73
|
+
# As a last attempt, try a simple GET without params appended (fallback)
|
|
74
|
+
try:
|
|
75
|
+
fallback_resp = self.session.get(url + "?" + "&".join(f"{k}={v}" for k, v in params.items()), headers=headers, timeout=self.timeout)
|
|
76
|
+
fallback_resp.raise_for_status()
|
|
77
|
+
return fallback_resp.text
|
|
78
|
+
except Exception:
|
|
79
|
+
# Final fallback: try using the requests library (less features but sometimes works)
|
|
80
|
+
try:
|
|
81
|
+
import requests
|
|
82
|
+
|
|
83
|
+
r = requests.get(url, params=params, headers=headers, timeout=self.timeout, verify=getattr(self, "verify", True))
|
|
84
|
+
r.raise_for_status()
|
|
85
|
+
return r.text
|
|
86
|
+
except Exception:
|
|
87
|
+
raise Exception(f"Failed to GET {url} with {params}: {exc}") from exc
|
|
88
|
+
raise Exception(f"Failed to GET {url} after retries: {last_exc}") from last_exc
|
|
89
|
+
|
|
90
|
+
# Pagination: offset param is a 0-based page index
|
|
91
|
+
while len(fetched_results) < max_results:
|
|
92
|
+
params = {"q": keywords, "source": "web", "offset": str(offset), "spellcheck": "0"}
|
|
93
|
+
if safesearch_value:
|
|
94
|
+
params["safesearch"] = safesearch_value
|
|
95
|
+
|
|
96
|
+
html = fetch_html(params)
|
|
97
|
+
# Parse and extract results using helper
|
|
98
|
+
page_results = self._parse_results_from_html(html)
|
|
99
|
+
|
|
100
|
+
if not page_results:
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
for res in page_results:
|
|
104
|
+
if len(fetched_results) >= max_results:
|
|
105
|
+
break
|
|
106
|
+
if res.href and res.href not in fetched_hrefs:
|
|
107
|
+
fetched_hrefs.add(res.href)
|
|
108
|
+
fetched_results.append(res)
|
|
109
|
+
|
|
110
|
+
offset += 1
|
|
111
|
+
if self.sleep_interval:
|
|
112
|
+
sleep(self.sleep_interval)
|
|
113
|
+
|
|
114
|
+
return fetched_results[:max_results]
|
|
115
|
+
|
|
116
|
+
def _parse_results_from_html(self, html: str) -> List[TextResult]:
|
|
117
|
+
"""Parse HTML and extract text search results.
|
|
118
|
+
|
|
119
|
+
This method is separated for testability.
|
|
120
|
+
"""
|
|
121
|
+
soup = Scout(html)
|
|
122
|
+
containers = soup.select("div.result-content")
|
|
123
|
+
results: List[TextResult] = []
|
|
124
|
+
|
|
125
|
+
for container in containers:
|
|
126
|
+
a_elem = container.select_one("a[href]")
|
|
127
|
+
# Title may be in .title.search-snippet-title or nested inside the anchor
|
|
128
|
+
title_elem = container.select_one(".title.search-snippet-title") or (a_elem.select_one(".title") if a_elem else None)
|
|
129
|
+
|
|
130
|
+
# Try multiple snippet locations: inside container, sibling .snippet, parent fallbacks
|
|
131
|
+
body = ""
|
|
132
|
+
candidates = []
|
|
133
|
+
# inside container
|
|
134
|
+
candidates.append(container.select_one(".generic-snippet"))
|
|
135
|
+
candidates.append(container.select_one(".snippet .generic-snippet"))
|
|
136
|
+
candidates.append(container.select_one(".description"))
|
|
137
|
+
candidates.append(container.select_one(".result-snippet"))
|
|
138
|
+
candidates.append(container.select_one("p"))
|
|
139
|
+
|
|
140
|
+
# sibling .snippet
|
|
141
|
+
try:
|
|
142
|
+
fn = getattr(container, "find_next_sibling", None)
|
|
143
|
+
if callable(fn):
|
|
144
|
+
sib = fn("div", class_="snippet")
|
|
145
|
+
if sib:
|
|
146
|
+
candidates.append(sib.select_one(".generic-snippet"))
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
# parent-level fallbacks
|
|
151
|
+
if container.parent:
|
|
152
|
+
candidates.append(container.parent.select_one(".snippet .generic-snippet"))
|
|
153
|
+
candidates.append(container.parent.select_one(".generic-snippet"))
|
|
154
|
+
|
|
155
|
+
for c in candidates:
|
|
156
|
+
if c:
|
|
157
|
+
text = c.get_text(strip=True)
|
|
158
|
+
if text:
|
|
159
|
+
body = text
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
if a_elem and title_elem:
|
|
163
|
+
href = a_elem.get("href", "").strip()
|
|
164
|
+
title = title_elem.get_text(strip=True)
|
|
165
|
+
results.append(TextResult(title=title, href=href, body=body))
|
|
166
|
+
|
|
167
|
+
return results
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""Brave videos search implementation.
|
|
2
|
+
|
|
3
|
+
This module provides a Brave video search engine that parses HTML responses
|
|
4
|
+
from Brave Search to extract video results from YouTube and other video platforms.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from webscout.search.engines.brave.videos import BraveVideos
|
|
8
|
+
>>> searcher = BraveVideos()
|
|
9
|
+
>>> results = searcher.run("python tutorial", max_results=10)
|
|
10
|
+
>>> for video in results:
|
|
11
|
+
... print(f"{video.title} - {video.url}")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from time import sleep
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from webscout.scout import Scout
|
|
20
|
+
|
|
21
|
+
from ....search.results import VideosResult
|
|
22
|
+
from .base import BraveBase
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BraveVideos(BraveBase):
|
|
26
|
+
"""Brave videos search engine.
|
|
27
|
+
|
|
28
|
+
Searches Brave Video Search and parses HTML responses to extract
|
|
29
|
+
video results including title, URL, thumbnail, duration, channel,
|
|
30
|
+
description, publish date, and view count.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
name: Engine identifier name.
|
|
34
|
+
provider: Provider identifier.
|
|
35
|
+
category: Search category type.
|
|
36
|
+
search_url: Base URL for video search.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name = "brave_videos"
|
|
40
|
+
provider = "brave"
|
|
41
|
+
category = "videos"
|
|
42
|
+
|
|
43
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
44
|
+
"""Initialize Brave videos search client.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
*args: Positional arguments passed to BraveBase.
|
|
48
|
+
**kwargs: Keyword arguments passed to BraveBase.
|
|
49
|
+
"""
|
|
50
|
+
super().__init__(*args, **kwargs)
|
|
51
|
+
self.search_url = f"{self.base_url}/videos"
|
|
52
|
+
|
|
53
|
+
def run(self, *args: Any, **kwargs: Any) -> list[VideosResult]:
|
|
54
|
+
"""Run video search on Brave.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
*args: Positional arguments. First arg is the search query.
|
|
58
|
+
**kwargs: Keyword arguments including:
|
|
59
|
+
- keywords: Search query string.
|
|
60
|
+
- region: Region code (e.g., 'us-en').
|
|
61
|
+
- safesearch: Safe search level ('on', 'moderate', 'off').
|
|
62
|
+
- max_results: Maximum number of results to return.
|
|
63
|
+
- timelimit: Time filter for results.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of VideosResult objects containing video information.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If no keywords are provided.
|
|
70
|
+
Exception: If the HTTP request fails.
|
|
71
|
+
"""
|
|
72
|
+
keywords = args[0] if args else kwargs.get("keywords")
|
|
73
|
+
region = args[1] if len(args) > 1 else kwargs.get("region", "us-en")
|
|
74
|
+
safesearch = args[2] if len(args) > 2 else kwargs.get("safesearch", "moderate")
|
|
75
|
+
max_results = args[3] if len(args) > 3 else kwargs.get("max_results", 10)
|
|
76
|
+
timelimit = kwargs.get("timelimit")
|
|
77
|
+
|
|
78
|
+
if max_results is None:
|
|
79
|
+
max_results = 10
|
|
80
|
+
|
|
81
|
+
if not keywords:
|
|
82
|
+
raise ValueError("Keywords are mandatory")
|
|
83
|
+
|
|
84
|
+
safesearch_map = {"on": "strict", "moderate": "moderate", "off": "off"}
|
|
85
|
+
safesearch_str = str(safesearch).lower() if safesearch else "moderate"
|
|
86
|
+
safesearch_value = safesearch_map.get(safesearch_str, "moderate")
|
|
87
|
+
|
|
88
|
+
fetched_results: list[VideosResult] = []
|
|
89
|
+
fetched_urls: set[str] = set()
|
|
90
|
+
|
|
91
|
+
offset = 0
|
|
92
|
+
while len(fetched_results) < max_results:
|
|
93
|
+
params: dict[str, str] = {
|
|
94
|
+
"q": keywords,
|
|
95
|
+
"source": "web",
|
|
96
|
+
"safesearch": safesearch_value,
|
|
97
|
+
"spellcheck": "0",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if offset > 0:
|
|
101
|
+
params["offset"] = str(offset)
|
|
102
|
+
|
|
103
|
+
if timelimit:
|
|
104
|
+
params["tf"] = timelimit
|
|
105
|
+
|
|
106
|
+
if region:
|
|
107
|
+
params["region"] = region
|
|
108
|
+
|
|
109
|
+
html = self._fetch_page(params)
|
|
110
|
+
page_results = self._parse_results_from_html(html)
|
|
111
|
+
|
|
112
|
+
if not page_results:
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
for result in page_results:
|
|
116
|
+
if len(fetched_results) >= max_results:
|
|
117
|
+
break
|
|
118
|
+
if result.url and result.url not in fetched_urls:
|
|
119
|
+
fetched_urls.add(result.url)
|
|
120
|
+
fetched_results.append(result)
|
|
121
|
+
|
|
122
|
+
offset += 1
|
|
123
|
+
|
|
124
|
+
if self.sleep_interval:
|
|
125
|
+
sleep(self.sleep_interval)
|
|
126
|
+
|
|
127
|
+
return fetched_results[:max_results]
|
|
128
|
+
|
|
129
|
+
def _fetch_page(self, params: dict[str, str]) -> str:
|
|
130
|
+
"""Fetch HTML page from Brave videos search.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
params: Query parameters for the request.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
HTML content of the response.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
Exception: If the request fails after retries.
|
|
140
|
+
"""
|
|
141
|
+
headers = dict(self.session.headers) if getattr(self, "session", None) else {}
|
|
142
|
+
headers.update({
|
|
143
|
+
"Referer": "https://search.brave.com/",
|
|
144
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
145
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
attempts = 3
|
|
149
|
+
backoff = 1.0
|
|
150
|
+
last_exc: Exception | None = None
|
|
151
|
+
|
|
152
|
+
for attempt in range(attempts):
|
|
153
|
+
try:
|
|
154
|
+
resp = self.session.get(
|
|
155
|
+
self.search_url,
|
|
156
|
+
params=params,
|
|
157
|
+
headers=headers,
|
|
158
|
+
timeout=self.timeout,
|
|
159
|
+
)
|
|
160
|
+
resp.raise_for_status()
|
|
161
|
+
return resp.text
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
last_exc = exc
|
|
164
|
+
try:
|
|
165
|
+
code = getattr(exc, "code", None) or getattr(exc, "status_code", None)
|
|
166
|
+
except Exception:
|
|
167
|
+
code = None
|
|
168
|
+
if code in (429, 500, 502, 503, 504):
|
|
169
|
+
sleep(backoff)
|
|
170
|
+
backoff *= 2
|
|
171
|
+
continue
|
|
172
|
+
raise Exception(f"Failed to GET {self.search_url}: {exc}") from exc
|
|
173
|
+
|
|
174
|
+
raise Exception(f"Failed to GET {self.search_url} after retries: {last_exc}") from last_exc
|
|
175
|
+
|
|
176
|
+
def _parse_results_from_html(self, html: str) -> list[VideosResult]:
|
|
177
|
+
"""Parse HTML and extract video search results.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
html: Raw HTML content from Brave videos search.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
List of VideosResult objects parsed from the HTML.
|
|
184
|
+
"""
|
|
185
|
+
soup = Scout(html)
|
|
186
|
+
results: list[VideosResult] = []
|
|
187
|
+
|
|
188
|
+
# Video results are in div.video-snippet containers
|
|
189
|
+
containers = soup.select("div.video-snippet")
|
|
190
|
+
|
|
191
|
+
for container in containers:
|
|
192
|
+
try:
|
|
193
|
+
result = self._parse_video_container(container)
|
|
194
|
+
if result and result.url:
|
|
195
|
+
results.append(result)
|
|
196
|
+
except Exception:
|
|
197
|
+
# Skip malformed results
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
return results
|
|
201
|
+
|
|
202
|
+
def _parse_video_container(self, container: Any) -> VideosResult | None:
|
|
203
|
+
"""Parse a single video container element.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
container: Scout element representing a video result container.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
VideosResult object or None if parsing fails.
|
|
210
|
+
"""
|
|
211
|
+
# Get video URL from main link
|
|
212
|
+
url = ""
|
|
213
|
+
link_elem = container.select_one("a[href]")
|
|
214
|
+
if link_elem:
|
|
215
|
+
url = link_elem.get("href", "").strip()
|
|
216
|
+
|
|
217
|
+
if not url:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
# Get thumbnail
|
|
221
|
+
thumbnail = ""
|
|
222
|
+
thumb_elem = container.select_one("img.thumb, img.video-thumb")
|
|
223
|
+
if thumb_elem:
|
|
224
|
+
thumbnail = thumb_elem.get("src", "").strip()
|
|
225
|
+
|
|
226
|
+
# Get duration
|
|
227
|
+
duration = ""
|
|
228
|
+
duration_elem = container.select_one(".over-thumbnail-info.duration")
|
|
229
|
+
if duration_elem:
|
|
230
|
+
duration = duration_elem.get_text(strip=True)
|
|
231
|
+
|
|
232
|
+
# Get title from the result header
|
|
233
|
+
title = ""
|
|
234
|
+
title_elem = container.select_one(".snippet-title")
|
|
235
|
+
if title_elem:
|
|
236
|
+
title = title_elem.get_text(strip=True)
|
|
237
|
+
|
|
238
|
+
# Get channel/uploader
|
|
239
|
+
uploader = ""
|
|
240
|
+
channel_elem = container.select_one(".attr.channel")
|
|
241
|
+
if channel_elem:
|
|
242
|
+
uploader = channel_elem.get_text(strip=True)
|
|
243
|
+
|
|
244
|
+
# Get provider (e.g., YouTube)
|
|
245
|
+
provider = ""
|
|
246
|
+
netloc_elem = container.select_one(".netloc.attr")
|
|
247
|
+
if netloc_elem:
|
|
248
|
+
provider = netloc_elem.get_text(strip=True)
|
|
249
|
+
|
|
250
|
+
# Get description
|
|
251
|
+
description = ""
|
|
252
|
+
desc_elem = container.select_one(".snippet-description, p.desc")
|
|
253
|
+
if desc_elem:
|
|
254
|
+
description = desc_elem.get_text(strip=True)
|
|
255
|
+
|
|
256
|
+
# Get publish date
|
|
257
|
+
published = ""
|
|
258
|
+
metrics_elem = container.select_one(".metrics")
|
|
259
|
+
if metrics_elem:
|
|
260
|
+
date_elem = metrics_elem.select_one(".attr:first-child")
|
|
261
|
+
if date_elem:
|
|
262
|
+
published = date_elem.get_text(strip=True)
|
|
263
|
+
|
|
264
|
+
# Get view count from metrics
|
|
265
|
+
view_count = 0
|
|
266
|
+
if metrics_elem:
|
|
267
|
+
view_elems = metrics_elem.select(".attr")
|
|
268
|
+
for elem in view_elems:
|
|
269
|
+
text = elem.get_text(strip=True)
|
|
270
|
+
# Look for view count patterns (e.g., "1.31M", "34.1M", "17K")
|
|
271
|
+
if any(c.isdigit() for c in text) and not any(
|
|
272
|
+
month in text.lower()
|
|
273
|
+
for month in [
|
|
274
|
+
"jan", "feb", "mar", "apr", "may", "jun",
|
|
275
|
+
"jul", "aug", "sep", "oct", "nov", "dec",
|
|
276
|
+
"hour", "day", "week", "month", "year", "ago",
|
|
277
|
+
]
|
|
278
|
+
):
|
|
279
|
+
view_count = self._parse_view_count(text)
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
return VideosResult(
|
|
283
|
+
title=title,
|
|
284
|
+
url=url,
|
|
285
|
+
thumbnail=thumbnail,
|
|
286
|
+
duration=duration,
|
|
287
|
+
uploader=uploader,
|
|
288
|
+
publisher=uploader,
|
|
289
|
+
provider=provider,
|
|
290
|
+
description=description,
|
|
291
|
+
published=published,
|
|
292
|
+
statistics={"views": view_count} if view_count else {},
|
|
293
|
+
content=description,
|
|
294
|
+
images={"thumbnail": thumbnail} if thumbnail else {},
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def _parse_view_count(self, text: str) -> int:
|
|
298
|
+
"""Parse view count from text like '1.31M' or '17K'.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
text: View count string with potential suffixes.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Integer view count, or 0 if parsing fails.
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
text = text.strip().upper()
|
|
308
|
+
multiplier = 1
|
|
309
|
+
|
|
310
|
+
if text.endswith("K"):
|
|
311
|
+
multiplier = 1000
|
|
312
|
+
text = text[:-1]
|
|
313
|
+
elif text.endswith("M"):
|
|
314
|
+
multiplier = 1_000_000
|
|
315
|
+
text = text[:-1]
|
|
316
|
+
elif text.endswith("B"):
|
|
317
|
+
multiplier = 1_000_000_000
|
|
318
|
+
text = text[:-1]
|
|
319
|
+
|
|
320
|
+
return int(float(text) * multiplier)
|
|
321
|
+
except (ValueError, TypeError):
|
|
322
|
+
return 0
|
|
323
|
+
|
|
324
|
+
def extract_results(self, html_text: str) -> list[VideosResult]:
|
|
325
|
+
"""Extract video results from HTML text.
|
|
326
|
+
|
|
327
|
+
This is an alias for _parse_results_from_html for API consistency.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
html_text: Raw HTML content from Brave videos search.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
List of VideosResult objects parsed from the HTML.
|
|
334
|
+
"""
|
|
335
|
+
return self._parse_results_from_html(html_text)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
if __name__ == "__main__":
|
|
339
|
+
# Test the BraveVideos search
|
|
340
|
+
print("Testing BraveVideos search...")
|
|
341
|
+
|
|
342
|
+
searcher = BraveVideos(timeout=15)
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
# Test basic search
|
|
346
|
+
results = searcher.run("python programming tutorial", max_results=5)
|
|
347
|
+
|
|
348
|
+
print(f"\nFound {len(results)} video results:\n")
|
|
349
|
+
for i, video in enumerate(results, 1):
|
|
350
|
+
print(f"{i}. {video.title}")
|
|
351
|
+
print(f" URL: {video.url}")
|
|
352
|
+
print(f" Duration: {video.duration}")
|
|
353
|
+
print(f" Channel: {video.uploader}")
|
|
354
|
+
print(f" Provider: {video.provider}")
|
|
355
|
+
print(f" Published: {video.published}")
|
|
356
|
+
if video.statistics:
|
|
357
|
+
print(f" Views: {video.statistics.get('views', 'N/A')}")
|
|
358
|
+
print(f" Thumbnail: {video.thumbnail[:80]}..." if video.thumbnail else "")
|
|
359
|
+
print()
|
|
360
|
+
|
|
361
|
+
except Exception as e:
|
|
362
|
+
print(f"Error during search: {e}")
|
|
363
|
+
import traceback
|
|
364
|
+
traceback.print_exc()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""DuckDuckGo search engines package."""
|
|
2
|
+
|
|
3
|
+
from .answers import DuckDuckGoAnswers
|
|
4
|
+
from .base import DuckDuckGoBase
|
|
5
|
+
from .images import DuckDuckGoImages
|
|
6
|
+
from .maps import DuckDuckGoMaps
|
|
7
|
+
from .news import DuckDuckGoNews
|
|
8
|
+
from .suggestions import DuckDuckGoSuggestions
|
|
9
|
+
from .text import DuckDuckGoTextSearch
|
|
10
|
+
from .translate import DuckDuckGoTranslate
|
|
11
|
+
from .videos import DuckDuckGoVideos
|
|
12
|
+
from .weather import DuckDuckGoWeather
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DuckDuckGoBase",
|
|
16
|
+
"DuckDuckGoTextSearch",
|
|
17
|
+
"DuckDuckGoImages",
|
|
18
|
+
"DuckDuckGoVideos",
|
|
19
|
+
"DuckDuckGoNews",
|
|
20
|
+
"DuckDuckGoAnswers",
|
|
21
|
+
"DuckDuckGoSuggestions",
|
|
22
|
+
"DuckDuckGoMaps",
|
|
23
|
+
"DuckDuckGoTranslate",
|
|
24
|
+
"DuckDuckGoWeather",
|
|
25
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""DuckDuckGo answers search."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import DuckDuckGoBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DuckDuckGoAnswers(DuckDuckGoBase):
|
|
9
|
+
"""DuckDuckGo instant answers."""
|
|
10
|
+
|
|
11
|
+
name = "duckduckgo"
|
|
12
|
+
category = "answers"
|
|
13
|
+
|
|
14
|
+
def run(self, *args, **kwargs) -> list[dict[str, str]]:
|
|
15
|
+
"""Get instant answers from DuckDuckGo.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
keywords: Search query.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of answer dictionaries.
|
|
22
|
+
"""
|
|
23
|
+
keywords = args[0] if args else kwargs.get("keywords")
|
|
24
|
+
|
|
25
|
+
assert keywords, "keywords is mandatory"
|
|
26
|
+
|
|
27
|
+
payload = {
|
|
28
|
+
"q": f"what is {keywords}",
|
|
29
|
+
"format": "json",
|
|
30
|
+
}
|
|
31
|
+
resp_content = self._get_url("GET", "https://api.duckduckgo.com/", params=payload).content
|
|
32
|
+
page_data = self.json_loads(resp_content)
|
|
33
|
+
|
|
34
|
+
results = []
|
|
35
|
+
answer = page_data.get("AbstractText")
|
|
36
|
+
url = page_data.get("AbstractURL")
|
|
37
|
+
if answer:
|
|
38
|
+
results.append(
|
|
39
|
+
{
|
|
40
|
+
"icon": None,
|
|
41
|
+
"text": answer,
|
|
42
|
+
"topic": None,
|
|
43
|
+
"url": url,
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# related
|
|
48
|
+
payload = {
|
|
49
|
+
"q": f"{keywords}",
|
|
50
|
+
"format": "json",
|
|
51
|
+
}
|
|
52
|
+
resp_content = self._get_url("GET", "https://api.duckduckgo.com/", params=payload).content
|
|
53
|
+
resp_json = self.json_loads(resp_content)
|
|
54
|
+
page_data = resp_json.get("RelatedTopics", [])
|
|
55
|
+
|
|
56
|
+
for row in page_data:
|
|
57
|
+
topic = row.get("Name")
|
|
58
|
+
if not topic:
|
|
59
|
+
icon = row["Icon"].get("URL")
|
|
60
|
+
results.append(
|
|
61
|
+
{
|
|
62
|
+
"icon": f"https://duckduckgo.com{icon}" if icon else "",
|
|
63
|
+
"text": row["Text"],
|
|
64
|
+
"topic": None,
|
|
65
|
+
"url": row["FirstURL"],
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
for subrow in row["Topics"]:
|
|
70
|
+
icon = subrow["Icon"].get("URL")
|
|
71
|
+
results.append(
|
|
72
|
+
{
|
|
73
|
+
"icon": f"https://duckduckgo.com{icon}" if icon else "",
|
|
74
|
+
"text": subrow["Text"],
|
|
75
|
+
"topic": topic,
|
|
76
|
+
"url": subrow["FirstURL"],
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return results
|