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
webscout/search/base.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Base class for search engines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
from typing import Any, Generic, Literal, Optional, TypeVar
|
|
9
|
+
|
|
10
|
+
from litprinter import ic
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from lxml import html
|
|
14
|
+
from lxml.etree import HTMLParser as LHTMLParser # type: ignore
|
|
15
|
+
LXML_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
LXML_AVAILABLE = False
|
|
18
|
+
from typing import Any
|
|
19
|
+
html: Any = None
|
|
20
|
+
LHTMLParser: Any = None
|
|
21
|
+
|
|
22
|
+
from .http_client import HttpClient
|
|
23
|
+
from .results import BooksResult, ImagesResult, NewsResult, TextResult, VideosResult
|
|
24
|
+
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BaseSearchEngine(ABC, Generic[T]):
|
|
29
|
+
"""Abstract base class for all search engine backends."""
|
|
30
|
+
|
|
31
|
+
name: str # unique key, e.g. "google"
|
|
32
|
+
category: Literal["text", "images", "videos", "news", "books", "suggestions", "weather", "maps", "translate"]
|
|
33
|
+
provider: str # source of the search results (e.g. "google", "bing", etc.)
|
|
34
|
+
disabled: bool = False # if True, the engine is disabled
|
|
35
|
+
priority: float = 1
|
|
36
|
+
|
|
37
|
+
search_url: str
|
|
38
|
+
search_method: str # GET or POST
|
|
39
|
+
search_headers: Mapping[str, str] = {}
|
|
40
|
+
items_xpath: str = ""
|
|
41
|
+
elements_xpath: Mapping[str, str] = {}
|
|
42
|
+
elements_replace: Mapping[str, str] = {}
|
|
43
|
+
|
|
44
|
+
def __init__(self, proxy: str | None = None, timeout: int | None = None, verify: bool = True):
|
|
45
|
+
"""Initialize search engine.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
proxy: Proxy URL (supports http/https/socks5).
|
|
49
|
+
timeout: Request timeout in seconds.
|
|
50
|
+
verify: Whether to verify SSL certificates.
|
|
51
|
+
"""
|
|
52
|
+
self.http_client = HttpClient(proxy=proxy, timeout=timeout, verify=verify)
|
|
53
|
+
self.results: list[T] = []
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def result_type(self) -> type[T]:
|
|
57
|
+
"""Get result type based on category."""
|
|
58
|
+
categories = {
|
|
59
|
+
"text": TextResult,
|
|
60
|
+
"images": ImagesResult,
|
|
61
|
+
"videos": VideosResult,
|
|
62
|
+
"news": NewsResult,
|
|
63
|
+
"books": BooksResult,
|
|
64
|
+
}
|
|
65
|
+
return categories[self.category] # type: ignore
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def build_payload(
|
|
69
|
+
self, query: str, region: str, safesearch: str, timelimit: str | None, page: int, **kwargs: Any
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
"""Build a payload for the search request."""
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
|
|
74
|
+
def request(self, method: str, url: str, **kwargs: Any) -> str | None:
|
|
75
|
+
"""Make a request to the search engine."""
|
|
76
|
+
try:
|
|
77
|
+
response = self.http_client.request(method, url, **kwargs) # type: ignore
|
|
78
|
+
return response.text
|
|
79
|
+
except Exception as ex:
|
|
80
|
+
ic.configureOutput(prefix='ERROR| ')
|
|
81
|
+
ic(f"Error in {self.name} request: {ex}")
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
@cached_property
|
|
85
|
+
def parser(self) -> Any:
|
|
86
|
+
"""Get HTML parser."""
|
|
87
|
+
if not LXML_AVAILABLE:
|
|
88
|
+
ic.configureOutput(prefix='WARNING| ')
|
|
89
|
+
ic("lxml not available, HTML parsing disabled")
|
|
90
|
+
return None
|
|
91
|
+
return LHTMLParser(remove_blank_text=True, remove_comments=True, remove_pis=True, collect_ids=False) if LHTMLParser else None
|
|
92
|
+
|
|
93
|
+
def extract_tree(self, html_text: str) -> Any:
|
|
94
|
+
"""Extract html tree from html text."""
|
|
95
|
+
if not LXML_AVAILABLE or not self.parser:
|
|
96
|
+
raise ImportError("lxml is required for HTML parsing")
|
|
97
|
+
assert self.parser is not None
|
|
98
|
+
assert html is not None
|
|
99
|
+
return html.fromstring(html_text, parser=self.parser)
|
|
100
|
+
|
|
101
|
+
def pre_process_html(self, html_text: str) -> str:
|
|
102
|
+
"""Pre-process html_text before extracting results."""
|
|
103
|
+
return html_text
|
|
104
|
+
|
|
105
|
+
def extract_results(self, html_text: str) -> list[T]:
|
|
106
|
+
"""Extract search results from html text."""
|
|
107
|
+
if not LXML_AVAILABLE:
|
|
108
|
+
raise ImportError("lxml is required for result extraction")
|
|
109
|
+
|
|
110
|
+
html_text = self.pre_process_html(html_text)
|
|
111
|
+
tree = self.extract_tree(html_text)
|
|
112
|
+
|
|
113
|
+
results = []
|
|
114
|
+
items = tree.xpath(self.items_xpath) if self.items_xpath else []
|
|
115
|
+
|
|
116
|
+
for item in items:
|
|
117
|
+
result = self.result_type()
|
|
118
|
+
for key, xpath in self.elements_xpath.items():
|
|
119
|
+
try:
|
|
120
|
+
data = item.xpath(xpath)
|
|
121
|
+
if data:
|
|
122
|
+
# Join text nodes or get first attribute
|
|
123
|
+
value = "".join(data) if isinstance(data, list) else data
|
|
124
|
+
setattr(result, key, value.strip() if isinstance(value, str) else value)
|
|
125
|
+
except Exception as ex:
|
|
126
|
+
ic.configureOutput(prefix='DEBUG| ')
|
|
127
|
+
ic(f"Error extracting {key}: {ex}")
|
|
128
|
+
results.append(result)
|
|
129
|
+
|
|
130
|
+
return results
|
|
131
|
+
|
|
132
|
+
def post_extract_results(self, results: list[T]) -> list[T]:
|
|
133
|
+
"""Post-process search results."""
|
|
134
|
+
return results
|
|
135
|
+
|
|
136
|
+
def search(
|
|
137
|
+
self,
|
|
138
|
+
query: str,
|
|
139
|
+
region: str = "us-en",
|
|
140
|
+
safesearch: str = "moderate",
|
|
141
|
+
timelimit: str | None = None,
|
|
142
|
+
page: int = 1,
|
|
143
|
+
**kwargs: Any,
|
|
144
|
+
) -> list[T] | None:
|
|
145
|
+
"""Search the engine."""
|
|
146
|
+
payload = self.build_payload(
|
|
147
|
+
query=query, region=region, safesearch=safesearch, timelimit=timelimit, page=page, **kwargs
|
|
148
|
+
)
|
|
149
|
+
if self.search_method == "GET":
|
|
150
|
+
html_text = self.request(self.search_method, self.search_url, params=payload, headers=self.search_headers)
|
|
151
|
+
else:
|
|
152
|
+
html_text = self.request(self.search_method, self.search_url, data=payload, headers=self.search_headers)
|
|
153
|
+
if not html_text:
|
|
154
|
+
return None
|
|
155
|
+
results = self.extract_results(html_text)
|
|
156
|
+
return self.post_extract_results(results)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Legacy base class for backwards compatibility
|
|
160
|
+
class BaseSearch(ABC):
|
|
161
|
+
"""Base class for synchronous search engines (legacy)."""
|
|
162
|
+
|
|
163
|
+
@abstractmethod
|
|
164
|
+
def text(self, *args, **kwargs) -> list[Any]:
|
|
165
|
+
"""Text search."""
|
|
166
|
+
raise NotImplementedError
|
|
167
|
+
|
|
168
|
+
@abstractmethod
|
|
169
|
+
def images(self, *args, **kwargs) -> list[Any]:
|
|
170
|
+
"""Images search."""
|
|
171
|
+
raise NotImplementedError
|
|
172
|
+
|
|
173
|
+
@abstractmethod
|
|
174
|
+
def videos(self, *args, **kwargs) -> list[Any]:
|
|
175
|
+
"""Videos search."""
|
|
176
|
+
raise NotImplementedError
|
|
177
|
+
|
|
178
|
+
@abstractmethod
|
|
179
|
+
def news(self, *args, **kwargs) -> list[Any]:
|
|
180
|
+
"""News search."""
|
|
181
|
+
raise NotImplementedError
|
|
182
|
+
|
|
183
|
+
@abstractmethod
|
|
184
|
+
def answers(self, *args, **kwargs) -> list[Any]:
|
|
185
|
+
"""Instant answers."""
|
|
186
|
+
raise NotImplementedError
|
|
187
|
+
|
|
188
|
+
@abstractmethod
|
|
189
|
+
def suggestions(self, *args, **kwargs) -> list[Any]:
|
|
190
|
+
"""Suggestions."""
|
|
191
|
+
raise NotImplementedError
|
|
192
|
+
|
|
193
|
+
@abstractmethod
|
|
194
|
+
def maps(self, *args, **kwargs) -> list[Any]:
|
|
195
|
+
"""Maps search."""
|
|
196
|
+
raise NotImplementedError
|
|
197
|
+
|
|
198
|
+
@abstractmethod
|
|
199
|
+
def translate(self, *args, **kwargs) -> list[Any]:
|
|
200
|
+
"""Translate."""
|
|
201
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Bing unified search interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from .base import BaseSearch
|
|
8
|
+
from .engines.bing.images import BingImagesSearch
|
|
9
|
+
from .engines.bing.news import BingNewsSearch
|
|
10
|
+
from .engines.bing.suggestions import BingSuggestionsSearch
|
|
11
|
+
from .engines.bing.text import BingTextSearch
|
|
12
|
+
from .results import ImagesResult, NewsResult, TextResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BingSearch(BaseSearch):
|
|
16
|
+
"""Unified Bing search interface."""
|
|
17
|
+
|
|
18
|
+
def text(self, keywords: str, region: str = "us", safesearch: str = "moderate", max_results: Optional[int] = None, unique: bool = True) -> List[TextResult]:
|
|
19
|
+
search = BingTextSearch()
|
|
20
|
+
return search.run(keywords, region, safesearch, max_results, unique=unique)
|
|
21
|
+
|
|
22
|
+
def images(self, keywords: str, region: str = "us", safesearch: str = "moderate", max_results: Optional[int] = None) -> List[ImagesResult]:
|
|
23
|
+
search = BingImagesSearch()
|
|
24
|
+
return search.run(keywords, region, safesearch, max_results)
|
|
25
|
+
|
|
26
|
+
def news(self, keywords: str, region: str = "us", safesearch: str = "moderate", max_results: Optional[int] = None) -> List[NewsResult]:
|
|
27
|
+
search = BingNewsSearch()
|
|
28
|
+
return search.run(keywords, region, safesearch, max_results)
|
|
29
|
+
|
|
30
|
+
def suggestions(self, query: str, region: str = "en-US") -> List[Dict[str, str]]:
|
|
31
|
+
search = BingSuggestionsSearch()
|
|
32
|
+
result = search.run(query, region)
|
|
33
|
+
return [{'suggestion': s} for s in result]
|
|
34
|
+
|
|
35
|
+
def answers(self, keywords: str) -> List[Dict[str, str]]:
|
|
36
|
+
raise NotImplementedError("Answers not implemented for Bing")
|
|
37
|
+
|
|
38
|
+
def maps(self, *args, **kwargs) -> List[Dict[str, str]]:
|
|
39
|
+
raise NotImplementedError("Maps not implemented for Bing")
|
|
40
|
+
|
|
41
|
+
def translate(self, keywords: str, from_lang: Optional[str] = None, to_lang: str = "en") -> List[Dict[str, str]]:
|
|
42
|
+
raise NotImplementedError("Translate not implemented for Bing")
|
|
43
|
+
|
|
44
|
+
def videos(self, *args, **kwargs) -> List[Dict[str, str]]:
|
|
45
|
+
raise NotImplementedError("Videos not implemented for Bing")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Brave unified search interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from .base import BaseSearch
|
|
8
|
+
from .engines.brave.images import BraveImages
|
|
9
|
+
from .engines.brave.news import BraveNews
|
|
10
|
+
from .engines.brave.suggestions import BraveSuggestions
|
|
11
|
+
from .engines.brave.text import BraveTextSearch
|
|
12
|
+
from .engines.brave.videos import BraveVideos
|
|
13
|
+
from .results import ImagesResult, NewsResult, TextResult, VideosResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BraveSearch(BaseSearch):
|
|
17
|
+
"""Unified Brave search interface."""
|
|
18
|
+
|
|
19
|
+
def text(
|
|
20
|
+
self,
|
|
21
|
+
keywords: str,
|
|
22
|
+
region: str = "us-en",
|
|
23
|
+
safesearch: str = "moderate",
|
|
24
|
+
max_results: Optional[int] = None,
|
|
25
|
+
) -> List[TextResult]:
|
|
26
|
+
search = BraveTextSearch()
|
|
27
|
+
return search.run(keywords, region, safesearch, max_results)
|
|
28
|
+
|
|
29
|
+
def images(
|
|
30
|
+
self,
|
|
31
|
+
keywords: str,
|
|
32
|
+
region: str = "us-en",
|
|
33
|
+
safesearch: str = "moderate",
|
|
34
|
+
max_results: Optional[int] = None,
|
|
35
|
+
) -> List[ImagesResult]:
|
|
36
|
+
"""Search Brave Images."""
|
|
37
|
+
search = BraveImages()
|
|
38
|
+
return search.run(keywords, region, safesearch, max_results)
|
|
39
|
+
|
|
40
|
+
def news(
|
|
41
|
+
self,
|
|
42
|
+
keywords: str,
|
|
43
|
+
region: str = "us-en",
|
|
44
|
+
safesearch: str = "moderate",
|
|
45
|
+
max_results: Optional[int] = None,
|
|
46
|
+
) -> List[NewsResult]:
|
|
47
|
+
"""Search Brave News."""
|
|
48
|
+
search = BraveNews()
|
|
49
|
+
return search.run(keywords, region, safesearch, max_results)
|
|
50
|
+
|
|
51
|
+
def suggestions(
|
|
52
|
+
self,
|
|
53
|
+
query: str,
|
|
54
|
+
rich: bool = True,
|
|
55
|
+
country: Optional[str] = None,
|
|
56
|
+
max_results: Optional[int] = None,
|
|
57
|
+
) -> List[Dict[str, str]]:
|
|
58
|
+
"""Search Brave suggestions/autocomplete."""
|
|
59
|
+
search = BraveSuggestions()
|
|
60
|
+
results = search.run(query, rich=rich, country=country, max_results=max_results)
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
"query": s.query,
|
|
64
|
+
"is_entity": str(s.is_entity),
|
|
65
|
+
"name": s.name,
|
|
66
|
+
"desc": s.desc,
|
|
67
|
+
}
|
|
68
|
+
for s in results
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
def answers(self, *args, **kwargs) -> List[Dict[str, str]]:
|
|
72
|
+
"""Instant answers not yet implemented for Brave."""
|
|
73
|
+
raise NotImplementedError("Brave instant answers not yet implemented")
|
|
74
|
+
|
|
75
|
+
def maps(self, *args, **kwargs) -> List[Dict[str, str]]:
|
|
76
|
+
"""Maps search not yet implemented for Brave."""
|
|
77
|
+
raise NotImplementedError("Brave maps search not yet implemented")
|
|
78
|
+
|
|
79
|
+
def translate(self, *args, **kwargs) -> List[Dict[str, str]]:
|
|
80
|
+
"""Translation not yet implemented for Brave."""
|
|
81
|
+
raise NotImplementedError("Brave translation not yet implemented")
|
|
82
|
+
|
|
83
|
+
def videos(
|
|
84
|
+
self,
|
|
85
|
+
keywords: str,
|
|
86
|
+
region: str = "us-en",
|
|
87
|
+
safesearch: str = "moderate",
|
|
88
|
+
max_results: Optional[int] = None,
|
|
89
|
+
) -> List[VideosResult]:
|
|
90
|
+
"""Search Brave Videos."""
|
|
91
|
+
search = BraveVideos()
|
|
92
|
+
return search.run(keywords, region, safesearch, max_results)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""DuckDuckGo unified search interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from .base import BaseSearch
|
|
8
|
+
from .engines.duckduckgo.answers import DuckDuckGoAnswers
|
|
9
|
+
from .engines.duckduckgo.images import DuckDuckGoImages
|
|
10
|
+
from .engines.duckduckgo.maps import DuckDuckGoMaps
|
|
11
|
+
from .engines.duckduckgo.news import DuckDuckGoNews
|
|
12
|
+
from .engines.duckduckgo.suggestions import DuckDuckGoSuggestions
|
|
13
|
+
from .engines.duckduckgo.text import DuckDuckGoTextSearch
|
|
14
|
+
from .engines.duckduckgo.translate import DuckDuckGoTranslate
|
|
15
|
+
from .engines.duckduckgo.videos import DuckDuckGoVideos
|
|
16
|
+
from .engines.duckduckgo.weather import DuckDuckGoWeather, WeatherData
|
|
17
|
+
from .results import ImagesResult, NewsResult, TextResult, VideosResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DuckDuckGoSearch(BaseSearch):
|
|
21
|
+
"""Unified DuckDuckGo search interface."""
|
|
22
|
+
|
|
23
|
+
def text(self, keywords: str, region: str = "wt-wt", safesearch: str = "moderate", timelimit: Optional[str] = None, backend: str = "api", max_results: Optional[int] = None) -> List[TextResult]:
|
|
24
|
+
search = DuckDuckGoTextSearch()
|
|
25
|
+
return search.run(keywords, region, safesearch, timelimit, backend, max_results)
|
|
26
|
+
|
|
27
|
+
def images(self, keywords: str, region: str = "wt-wt", safesearch: str = "moderate", timelimit: Optional[str] = None, size: Optional[str] = None, color: Optional[str] = None, type_image: Optional[str] = None, layout: Optional[str] = None, license_image: Optional[str] = None, max_results: Optional[int] = None) -> List[ImagesResult]:
|
|
28
|
+
search = DuckDuckGoImages()
|
|
29
|
+
return search.run(keywords, region, safesearch, timelimit, size, color, type_image, layout, license_image, max_results)
|
|
30
|
+
|
|
31
|
+
def videos(self, keywords: str, region: str = "wt-wt", safesearch: str = "moderate", timelimit: Optional[str] = None, resolution: Optional[str] = None, duration: Optional[str] = None, license_videos: Optional[str] = None, max_results: Optional[int] = None) -> List[VideosResult]:
|
|
32
|
+
search = DuckDuckGoVideos()
|
|
33
|
+
return search.run(keywords, region, safesearch, timelimit, resolution, duration, license_videos, max_results)
|
|
34
|
+
|
|
35
|
+
def news(self, keywords: str, region: str = "wt-wt", safesearch: str = "moderate", timelimit: Optional[str] = None, max_results: Optional[int] = None) -> List[NewsResult]:
|
|
36
|
+
search = DuckDuckGoNews()
|
|
37
|
+
return search.run(keywords, region, safesearch, timelimit, max_results)
|
|
38
|
+
|
|
39
|
+
def answers(self, keywords: str) -> List[Dict[str, str]]:
|
|
40
|
+
search = DuckDuckGoAnswers()
|
|
41
|
+
return search.run(keywords)
|
|
42
|
+
|
|
43
|
+
def suggestions(self, keywords: str, region: str = "wt-wt") -> List[Dict[str, str]]:
|
|
44
|
+
search = DuckDuckGoSuggestions()
|
|
45
|
+
return search.run(keywords, region)
|
|
46
|
+
|
|
47
|
+
def maps(self, keywords: str, place: Optional[str] = None, street: Optional[str] = None, city: Optional[str] = None, county: Optional[str] = None, state: Optional[str] = None, country: Optional[str] = None, postalcode: Optional[str] = None, latitude: Optional[str] = None, longitude: Optional[str] = None, radius: int = 0, max_results: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
48
|
+
search = DuckDuckGoMaps()
|
|
49
|
+
return search.run(keywords, place, street, city, county, state, country, postalcode, latitude, longitude, radius, max_results)
|
|
50
|
+
|
|
51
|
+
def translate(self, keywords: str, from_lang: Optional[str] = None, to_lang: str = "en") -> List[Dict[str, str]]:
|
|
52
|
+
search = DuckDuckGoTranslate()
|
|
53
|
+
return search.run(keywords, from_lang, to_lang)
|
|
54
|
+
|
|
55
|
+
def weather(self, keywords: str) -> WeatherData:
|
|
56
|
+
search = DuckDuckGoWeather()
|
|
57
|
+
return search.run(keywords)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Static imports for all search engine modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..base import BaseSearchEngine
|
|
6
|
+
from .bing import BingBase, BingImagesSearch, BingNewsSearch, BingSuggestionsSearch, BingTextSearch
|
|
7
|
+
from .brave import (
|
|
8
|
+
BraveBase,
|
|
9
|
+
BraveImages,
|
|
10
|
+
BraveNews,
|
|
11
|
+
BraveSuggestions,
|
|
12
|
+
BraveTextSearch,
|
|
13
|
+
BraveVideos,
|
|
14
|
+
)
|
|
15
|
+
from .duckduckgo import (
|
|
16
|
+
DuckDuckGoAnswers,
|
|
17
|
+
DuckDuckGoBase,
|
|
18
|
+
DuckDuckGoImages,
|
|
19
|
+
DuckDuckGoMaps,
|
|
20
|
+
DuckDuckGoNews,
|
|
21
|
+
DuckDuckGoSuggestions,
|
|
22
|
+
DuckDuckGoTextSearch,
|
|
23
|
+
DuckDuckGoTranslate,
|
|
24
|
+
DuckDuckGoVideos,
|
|
25
|
+
DuckDuckGoWeather,
|
|
26
|
+
)
|
|
27
|
+
from .mojeek import Mojeek
|
|
28
|
+
from .wikipedia import Wikipedia
|
|
29
|
+
from .yahoo import (
|
|
30
|
+
YahooImages,
|
|
31
|
+
YahooNews,
|
|
32
|
+
YahooSearchEngine,
|
|
33
|
+
YahooSuggestions,
|
|
34
|
+
YahooText,
|
|
35
|
+
YahooVideos,
|
|
36
|
+
)
|
|
37
|
+
from .yandex import Yandex
|
|
38
|
+
from .yep import YepBase, YepImages, YepSuggestions, YepTextSearch
|
|
39
|
+
|
|
40
|
+
# Engine categories mapping
|
|
41
|
+
ENGINES = {
|
|
42
|
+
"text": {
|
|
43
|
+
"brave": BraveTextSearch,
|
|
44
|
+
"mojeek": Mojeek,
|
|
45
|
+
"yandex": Yandex,
|
|
46
|
+
"bing": BingTextSearch,
|
|
47
|
+
"duckduckgo": DuckDuckGoTextSearch,
|
|
48
|
+
"yep": YepTextSearch,
|
|
49
|
+
"yahoo": YahooText,
|
|
50
|
+
},
|
|
51
|
+
"images": {
|
|
52
|
+
"bing": BingImagesSearch,
|
|
53
|
+
"brave": BraveImages,
|
|
54
|
+
"duckduckgo": DuckDuckGoImages,
|
|
55
|
+
"yep": YepImages,
|
|
56
|
+
"yahoo": YahooImages,
|
|
57
|
+
},
|
|
58
|
+
"videos": {
|
|
59
|
+
"brave": BraveVideos,
|
|
60
|
+
"duckduckgo": DuckDuckGoVideos,
|
|
61
|
+
"yahoo": YahooVideos,
|
|
62
|
+
},
|
|
63
|
+
"news": {
|
|
64
|
+
"brave": BraveNews,
|
|
65
|
+
"bing": BingNewsSearch,
|
|
66
|
+
"duckduckgo": DuckDuckGoNews,
|
|
67
|
+
"yahoo": YahooNews,
|
|
68
|
+
},
|
|
69
|
+
"suggestions": {
|
|
70
|
+
"brave": BraveSuggestions,
|
|
71
|
+
"bing": BingSuggestionsSearch,
|
|
72
|
+
"duckduckgo": DuckDuckGoSuggestions,
|
|
73
|
+
"yep": YepSuggestions,
|
|
74
|
+
"yahoo": YahooSuggestions,
|
|
75
|
+
},
|
|
76
|
+
"answers": {
|
|
77
|
+
"duckduckgo": DuckDuckGoAnswers,
|
|
78
|
+
},
|
|
79
|
+
"maps": {
|
|
80
|
+
"duckduckgo": DuckDuckGoMaps,
|
|
81
|
+
},
|
|
82
|
+
"translate": {
|
|
83
|
+
"duckduckgo": DuckDuckGoTranslate,
|
|
84
|
+
},
|
|
85
|
+
"weather": {
|
|
86
|
+
"duckduckgo": DuckDuckGoWeather,
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
__all__ = [
|
|
91
|
+
"BraveBase",
|
|
92
|
+
"BraveTextSearch",
|
|
93
|
+
"BraveImages",
|
|
94
|
+
"BraveVideos",
|
|
95
|
+
"BraveNews",
|
|
96
|
+
"BraveSuggestions",
|
|
97
|
+
"Mojeek",
|
|
98
|
+
"Wikipedia",
|
|
99
|
+
"Yandex",
|
|
100
|
+
"BingBase",
|
|
101
|
+
"BingTextSearch",
|
|
102
|
+
"BingImagesSearch",
|
|
103
|
+
"BingNewsSearch",
|
|
104
|
+
"BingSuggestionsSearch",
|
|
105
|
+
"DuckDuckGoBase",
|
|
106
|
+
"DuckDuckGoTextSearch",
|
|
107
|
+
"DuckDuckGoImages",
|
|
108
|
+
"DuckDuckGoVideos",
|
|
109
|
+
"DuckDuckGoNews",
|
|
110
|
+
"DuckDuckGoAnswers",
|
|
111
|
+
"DuckDuckGoSuggestions",
|
|
112
|
+
"DuckDuckGoMaps",
|
|
113
|
+
"DuckDuckGoTranslate",
|
|
114
|
+
"DuckDuckGoWeather",
|
|
115
|
+
"YepBase",
|
|
116
|
+
"YepTextSearch",
|
|
117
|
+
"YepImages",
|
|
118
|
+
"YepSuggestions",
|
|
119
|
+
"YahooSearchEngine",
|
|
120
|
+
"YahooText",
|
|
121
|
+
"YahooImages",
|
|
122
|
+
"YahooVideos",
|
|
123
|
+
"YahooNews",
|
|
124
|
+
"YahooSuggestions",
|
|
125
|
+
"BaseSearchEngine",
|
|
126
|
+
"ENGINES",
|
|
127
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Bing search engines."""
|
|
2
|
+
|
|
3
|
+
from .base import BingBase
|
|
4
|
+
from .images import BingImagesSearch
|
|
5
|
+
from .news import BingNewsSearch
|
|
6
|
+
from .suggestions import BingSuggestionsSearch
|
|
7
|
+
from .text import BingTextSearch
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BingBase",
|
|
11
|
+
"BingTextSearch",
|
|
12
|
+
"BingImagesSearch",
|
|
13
|
+
"BingNewsSearch",
|
|
14
|
+
"BingSuggestionsSearch",
|
|
15
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Base class for Bing search implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from curl_cffi.requests import Session
|
|
6
|
+
|
|
7
|
+
from ....litagent import LitAgent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BingBase:
|
|
11
|
+
"""Base class for Bing search engines."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
timeout: int = 10,
|
|
16
|
+
proxies: dict[str, str] | None = None,
|
|
17
|
+
verify: bool = True,
|
|
18
|
+
lang: str = "en-US",
|
|
19
|
+
sleep_interval: float = 0.0,
|
|
20
|
+
impersonate: str = "chrome110",
|
|
21
|
+
):
|
|
22
|
+
self.timeout = timeout
|
|
23
|
+
self.proxies = proxies
|
|
24
|
+
self.verify = verify
|
|
25
|
+
self.lang = lang
|
|
26
|
+
self.sleep_interval = sleep_interval
|
|
27
|
+
self.base_url = "https://www.bing.com"
|
|
28
|
+
from typing import Any, Optional, cast
|
|
29
|
+
self.session = Session(
|
|
30
|
+
proxies=cast(Any, proxies),
|
|
31
|
+
verify=verify,
|
|
32
|
+
timeout=timeout,
|
|
33
|
+
impersonate=cast(Any, impersonate),
|
|
34
|
+
)
|
|
35
|
+
self.session.headers.update(LitAgent().generate_fingerprint())
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Bing images search."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from time import sleep
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
from urllib.parse import urlencode
|
|
8
|
+
|
|
9
|
+
from webscout.scout import Scout
|
|
10
|
+
from webscout.search.results import ImagesResult
|
|
11
|
+
|
|
12
|
+
from .base import BingBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BingImagesSearch(BingBase):
|
|
16
|
+
name = "bing"
|
|
17
|
+
category = "images"
|
|
18
|
+
def run(self, *args, **kwargs) -> List[ImagesResult]:
|
|
19
|
+
keywords = args[0] if args else kwargs.get("keywords")
|
|
20
|
+
args[1] if len(args) > 1 else kwargs.get("region", "us")
|
|
21
|
+
safesearch = args[2] if len(args) > 2 else kwargs.get("safesearch", "moderate")
|
|
22
|
+
max_results = args[3] if len(args) > 3 else kwargs.get("max_results", 10)
|
|
23
|
+
|
|
24
|
+
if max_results is None:
|
|
25
|
+
max_results = 10
|
|
26
|
+
|
|
27
|
+
if not keywords:
|
|
28
|
+
raise ValueError("Keywords are mandatory")
|
|
29
|
+
|
|
30
|
+
safe_map = {
|
|
31
|
+
"on": "Strict",
|
|
32
|
+
"moderate": "Moderate",
|
|
33
|
+
"off": "Off"
|
|
34
|
+
}
|
|
35
|
+
safe_map.get(safesearch.lower(), "Moderate")
|
|
36
|
+
|
|
37
|
+
# Bing images URL
|
|
38
|
+
url = f"{self.base_url}/images/async"
|
|
39
|
+
params = {
|
|
40
|
+
'q': keywords,
|
|
41
|
+
'first': '1',
|
|
42
|
+
'count': '35', # Fetch more to get max_results
|
|
43
|
+
'cw': '1177',
|
|
44
|
+
'ch': '759',
|
|
45
|
+
'tsc': 'ImageHoverTitle',
|
|
46
|
+
'layout': 'RowBased_Landscape',
|
|
47
|
+
't': '0',
|
|
48
|
+
'IG': '',
|
|
49
|
+
'SFX': '0',
|
|
50
|
+
'iid': 'images.1'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
results = []
|
|
54
|
+
first = 1
|
|
55
|
+
sfx = 0
|
|
56
|
+
|
|
57
|
+
while len(results) < max_results:
|
|
58
|
+
params['first'] = str(first)
|
|
59
|
+
params['SFX'] = str(sfx)
|
|
60
|
+
full_url = f"{url}?{urlencode(params)}"
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
response = self.session.get(full_url, timeout=self.timeout)
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
html = response.text
|
|
66
|
+
except Exception as e:
|
|
67
|
+
raise Exception(f"Failed to fetch images: {str(e)}")
|
|
68
|
+
|
|
69
|
+
soup = Scout(html)
|
|
70
|
+
img_tags = soup.select('a.iusc img')
|
|
71
|
+
|
|
72
|
+
for img in img_tags:
|
|
73
|
+
if len(results) >= max_results:
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
title = img.get('alt', '')
|
|
77
|
+
src = img.get('src', '')
|
|
78
|
+
m_attr = img.parent.get('m', '') if img.parent else ''
|
|
79
|
+
|
|
80
|
+
# Parse m attribute for full image URL
|
|
81
|
+
image_url = src
|
|
82
|
+
thumbnail = src
|
|
83
|
+
if m_attr:
|
|
84
|
+
try:
|
|
85
|
+
import json
|
|
86
|
+
m_data = json.loads(m_attr)
|
|
87
|
+
image_url = m_data.get('murl', src)
|
|
88
|
+
thumbnail = m_data.get('turl', src)
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
source = ''
|
|
93
|
+
if img.parent and img.parent.parent:
|
|
94
|
+
source_tag = img.parent.parent.select_one('.iusc .lnk')
|
|
95
|
+
if source_tag:
|
|
96
|
+
source = source_tag.get_text(strip=True)
|
|
97
|
+
|
|
98
|
+
results.append(ImagesResult(
|
|
99
|
+
title=title,
|
|
100
|
+
image=image_url,
|
|
101
|
+
thumbnail=thumbnail,
|
|
102
|
+
url=image_url,
|
|
103
|
+
height=0,
|
|
104
|
+
width=0,
|
|
105
|
+
source=source
|
|
106
|
+
))
|
|
107
|
+
|
|
108
|
+
first += 35
|
|
109
|
+
sfx += 1
|
|
110
|
+
|
|
111
|
+
if self.sleep_interval:
|
|
112
|
+
sleep(self.sleep_interval)
|
|
113
|
+
|
|
114
|
+
return results[:max_results]
|